[toc]
MybatisPlus笔记 版本3.5.2
当前MybatisPlus版本为3.5.2
MyBatisPlus(简称MP)是基于MyBatis框架基础上开发的增强型工具,旨在简化开发、提高效率。
MP的特性:
- 无侵入:只做增强不做改变,不会对现有工程产生影响。
- 强大的 CRUD 操作:内置通用 Mapper,少量配置即可实现单表CRUD 操作。
- 支持 Lambda:编写查询条件无需担心字段写错。
- 支持主键自动生成。
- 内置分页插件。
1 MybatisPlus入门案例
1 创建SpringBoot项目
创建SpringBoot项目,选择导入Spring Web,Mysql,MybatisPlus相关依赖
- MybatisPlus依赖包含了Mybatis依赖,无须二次引入Mybatis依赖。
完整依赖
<dependencies>
<!-- spring web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MybatisPlus依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- Mysql依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
2 创建数据库表,实体类
- 创建数据库test,其中创建表book
CREATE TABLE `book` (
`id` int NOT NULL,
`name` varchar(255) DEFAULT NULL,
`type` varchar(255) DEFAULT NULL,
`description` varchar(255) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 创建entity包下。其中根据表book创建实体类Book,内容如下
public class Book {
private Integer id;
private String name;
private String type;
private String description;
//setter and getter
//toString
}
3 定义dao接口与@Mapper注解
创建BookDao接口,接口继承BaseMapper接口,内容如下
//@Mapper让该dao接口能注入到spring容器
@Mapper
public interface BookDao extends BaseMapper<Book>{
//无须编写具体的方法
//BookDao接口从BaseMapper接口中继承了许多通用的CRUD方法
}
- dao接口继承BaseMapper接口,会继承许多通用的CRUD方法。
说明:Dao接口要想被SpringIOC容器扫描到,有两种解决方案:
- 方案一:在Dao接口上添加@Mapper注解,并且确保Dao处在引导类(启动类)所在包或其子包中。该方案的缺点是需要在每个Dao接口中添加注解。
- 方案二:在引导类上添加@MapperScan注解,其属性为所要扫描的Dao所在包。该方案的好处是只需要写一次,则指定包下的所有Dao接口都能被扫描到,@Mapper注解就可以不写。
- 通常使用更多的是方案一。
//通过@MapperScan注解扫描包。把dao接口注入到spring容器中
@SpringBootApplication
@MapperScan("com.example.springbootdemo3.dao")
public class Springbootdemo3Application {
public static void main(String[] args) {
SpringApplication.run(Springbootdemo3Application.class, args);
}
}
4 定义service接口及其实现类
创建service包下。其中创建BookService接口及其实现类,内容如下
public interface BookService {
public Book getById(Integer id);
}
//////////////////
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao dao;
@Override
public Book getById(Integer id) {
//selectById方法是dao接口从BaseMapper接口中继承过来的方法
return dao.selectById(id);
}
}
- dao接口继承BaseMapper接口,会继承许多通用的CRUD方法。
5 定义controller层
创建controller包下。其中创建BookController实现类,内容如下
@RestController
@RequestMapping("/book")
public class BookController {
@Autowired
private BookService bookService;
@RequestMapping("/getById/{id}")
public Book getById(@PathVariable("id") Integer id){
return bookService.getById(id);
}
}
6 添加数据源配置信息
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC
username: root
password: root
7 运行启动类,测试接口
2 MybatisPlus数据层(dao层)开发
- MyBatisPlus的BaseMapper接口提供了一些标准的CRUD方法。可以直接让dao接口继承。从而简化dao接口代码。
如下案例:
//@Mapper让该dao接口能注入到spring容器
@Mapper
public interface BookDao extends BaseMapper<Book>{
//无须编写具体的方法
//BookDao接口从BaseMapper接口中继承了许多通用的CRUD方法
}
下图是BaseMapper接口中部分提供的标准CRUD方法:
以下是BaseMapper接口源代码:
public interface BaseMapper<T> extends Mapper<T> {
//int:返回值,新增成功后返回1,没有新增成功返回的是0
//T:泛型,新增数据
int insert(T entity);
//int:返回值类型,数据删除成功返回1,未删除数据返回0
int deleteById(Serializable id);
//T:泛型,需要修改的数据内容,注意因为根据ID进行修改,所以传入的对象entity中需要有ID属性值
//int:返回值,修改成功后返回1,未修改数据返回0
//注意:修改的时候,只修改实体对象entity中有值的字段。
int updateById(@Param("et") T entity);
//Serializable:参数类型,主键ID的值
//T:根据ID查询只会返回一条数据
T selectById(Serializable id);
//Wrapper:查询条件对象,没有可直接传为Null
//List:因为查询的是所有,所以返回的数据是一个集合
List<T> selectList(@Param("ew") Wrapper<T> queryWrapper);
//分页方法
<P extends IPage<T>> P selectPage(P page, @Param("ew") Wrapper<T> queryWrapper);
}
1 分页功能
MP中如何实现分页功能?
BaseMapper接口中分页查询使用的方法是:
<P extends IPage<T>> P selectPage(P page, @Param("ew") Wrapper<T> queryWrapper);
- IPage:用来构建分页查询条件
- Wrapper:用来构建条件查询的条件,没有可直接传为Null
- IPage:返回值,形参和方法的返回值都是IPage
① 步骤1:编写分页方法。
public interface BookService {
public IPage<Book> selectPage();
}
///////////
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao dao;
//测试分页方法
public IPage<Book> selectPage(){
//1 创建IPage分页对象,设置分页参数,1为当前页码,3为每页显示的记录数
IPage<Book> page=new Page<>(1,10);
//2 执行分页查询
IPage<Book> bookIPage = dao.selectPage(page, null);
//3 获取分页结果
System.out.println("当前页码值:"+page.getCurrent());
System.out.println("每页显示数:"+page.getSize());
System.out.println("一共多少页:"+page.getPages());
System.out.println("一共多少条数据:"+page.getTotal());
System.out.println("分页数据:"+page.getRecords());
return page;
}
}
////////
@RequestMapping("/book")
public class BookController {
@Autowired
private BookService bookService;
@RequestMapping("/selectPage")
public IPage<Book> selectPage(){
return bookService.selectPage();
}
}
② 步骤2:设置分页拦截器
这个拦截器MP已经为我们提供好了,我们只需要将其配置成Spring管理的bean对象即可。
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
//1 创建MybatisPlusInterceptor拦截器对象
MybatisPlusInterceptor mpInterceptor=new MybatisPlusInterceptor();
//2 添加分页拦截器
mpInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mpInterceptor;
}
}
③ 步骤3 测试
3 条件构造器Wrapper
MyBatisPlus将书写复杂的SQL条件进行了封装。Wrapper类就是用来封装条件的。具体用法参考官方文档。
1 查询条件构造器QueryWrapper
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao dao;
public void test(){
QueryWrapper qw = new QueryWrapper();
qw.lt("id",10);
qw.gt("id",5);
List<Book> bookList = dao.selectList(qw);
System.out.println(bookList);
}
}
- lt: 小于(<)
- gt: 大于(>)
//最终的SQL语句如下
SELECT * FROM book WHERE (age < 10 AND age > 5)
2 查询指定字段
在查询数据的时候,什么都没有做默认就是查询表中所有字段的内容,select(...)方法用来设置查询的字段列。
void test(){
QueryWrapper<User> lqw = new QueryWrapper<User>();
lqw.select("id","name","age","tel");
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
}
- 最终的sql语句为: SELECT id,name,age,tel FROM user
3 聚合查询
select(...)方法可以用来设置聚合函数查询。
count:总记录数
max:最大值
min:最小值
avg:平均值
sum:求和
void test(){
QueryWrapper<User> lqw = new QueryWrapper<User>();
//lqw.select("count(*) as count");
//SELECT count(*) as count FROM user
//lqw.select("max(age) as maxAge");
//SELECT max(age) as maxAge FROM user
//lqw.select("min(age) as minAge");
//SELECT min(age) as minAge FROM user
//lqw.select("sum(age) as sumAge");
//SELECT sum(age) as sumAge FROM user
lqw.select("avg(age) as avgAge");
//SELECT avg(age) as avgAge FROM user
List<Map<String, Object>> userList = userDao.selectMaps(lqw);
System.out.println(userList);
}
4 分组查询
select()方法与groupBy()方法可用于分组查询。
void test(){
QueryWrapper<User> lqw = new QueryWrapper<User>();
lqw.select("count(*) as count,tel");
lqw.groupBy("tel");
List<Map<String, Object>> list = userDao.selectMaps(lqw);
System.out.println(list);
}
- groupBy为分组,最终的sql语句如下:
SELECT count(*) as count,tel FROM user GROUP BY tel
注意:
- 聚合与分组查询,无法使用lambda表达式来完成
- MP只是对MyBatis的增强,如果MP实现不了,可以直接在DAO接口中使用MyBatis的方式实现。
5 其余查询条件
除了lt()和gt()方法外。MP的查询条件方法有很多:
- 范围匹配(> 、 = 、between)
- 模糊匹配(like)
- 空判定(null)
- 包含性匹配(in)
- 分组(group)
- 排序(order)
1 等值查询
void testGetAll(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.eq(User::getName, "Jerry").eq(User::getPassword, "jerry");
User loginUser = userDao.selectOne(lqw);
System.out.println(loginUser);
}
- eq(): 相当于
=
,对应的sql语句为
SELECT * FROM user WHERE (name = ? AND password = ?)
2 范围查询
void test(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.between(User::getAge, 10, 30);
//SELECT * FROM user WHERE (age BETWEEN ? AND ?)
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
}
- gt():大于(>)
- ge():大于等于(>=)
- lt():小于(<)
- lte():小于等于(<=)
- between():between ? and ?
3 模糊查询
void testGetAll(){
LambdaQueryWrapper<User> lqw = new LambdaQueryWrapper<User>();
lqw.likeLeft(User::getName, "J");
//SELECT * FROM user WHERE (name LIKE ?)
List<User> userList = userDao.selectList(lqw);
System.out.println(userList);
}
- like():前后加百分号,如 %J%
- likeLeft():前面加百分号,如 %J
- likeRight():后面加百分号,如 J%
4 排序查询
void testGetAll(){
LambdaQueryWrapper<User> lwq = new LambdaQueryWrapper<>();
/**
* condition :条件,返回boolean,
当condition为true,进行排序,如果为false,则不排序
* isAsc:是否为升序,true为升序,false为降序
* columns:需要操作的列
*/
lwq.orderBy(true,false, User::getId);
List<User> userList = userDao.selectList(lwq);
System.out.println(userList);
}
- orderBy排序
- condition:条件,true则添加排序,false则不添加排序
- isAsc:是否为升序,true升序,false降序
- columns:排序字段,可以有多个
- orderByAsc/Desc(单个column):按照指定字段进行升序/降序
- orderByAsc/Desc(多个column):按照多个字段进行升序/降序
- orderByAsc/Desc
- condition:条件,true添加排序,false不添加排序
- 多个columns:按照多个字段进行排序
4 实体类属性名和表的列名之间的映射关系
查询出来的数据能够成功的从表中获取并封装到实体类对象中,原因是表的字段列名和实体类的属性名一样。
问题1:若表字段与编码属性设计不同步
当表的列名和模型类的属性名发生不一致,就会导致数据封装不到模型对象,这个时候就需要其中一方做出修改,那如果前提是两边都不能改又该如何解决?
MP给我们提供了一个注解@TableField,使用该注解可以实现模型类属性名和表的列名之间的映射关系
问题2:若编码中添加了数据库中未定义的属性
当模型类中多了一个数据库表不存在的字段,就会导致生成的sql语句中在select的时候查询了数据库不存在的字段,程序运行就会报错,错误信息为:
Unknown column '多出来的字段名称' in 'field list'
具体的解决方案用到的还是@TableField注解,它有一个属性叫exist,设置该字段是否在数据库表中存在,如果设置为false则不存在,生成sql语句查询的时候,就不会再查询该字段了。
问题3:若采用默认查询开放了更多的字段查看权限
查询表中所有的列的数据,就可能把一些敏感数据查询到返回给前端,例如密码。这个时候我们就需要限制哪些字段默认不要进行查询。解决方案是@TableField注解的一个属性叫select,该属性设置默认是否需要查询该字段的值,true(默认值)表示默认查询该字段,false表示默认不查询该字段。
问题4:若表名与模型类名称不同
该问题主要是表的名称和模型类的名称不一致,会导致查询失败。解决方案是使用MP提供的另外一个注解@TableName来设置表与模型类之间的对应关系。
小结:@TableField,@TableName注解
@TableField注解 类型:属性注解 位置:属性上方定义 作用:设置当前属性对应的数据库表中的字段关系
@TableName注解 类型:类注解 位置:类上方定义 作用:设置当前类对应于数据库表关系
5 ID生成策略与@TableId注解
MybatisPlus提供以下几种ID生成策略:
- AUTO: id自增长策略。需要对应表设置了ID主键自增,否则无效。
- NONE: 不设置id生成策略
- INPUT:用户手工输入id
- ASSIGN_ID:雪花算法生成id(可兼容数值型与字符串型)
- ASSIGN_UUID:以UUID生成算法作为id生成策略
AUTO: id自增长策略
当新增数据到表中的时候。无须传入id值。MybatisPlus框架会读取表中最大的id值,在其基础上自增+1。作为新增数据的id值。
- AUTO的作用是使用数据库ID自增,在使用该策略的时候一定要确保对应的数据库表设置了ID主键自增,否则无效。
@Data
@TableName("tbl_user")
public class User {
//设置tbl_user表的id,为AUTO策略
@TableId(type = IdType.AUTO)
private Long id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}
//////////////////////////
public void testSave(){
//新增数据的时候,不用设置id值
User user = new User();
user.setName("xiaoming");
user.setPassword("123456");
user.setAge(12);
user.setTel("4006184000");
userDao.insert(user);
}
INPUT策略
INPUT策略:插入数据的时候需要主动输入id。否则报错。
- 使用INPUT策略的时候要确保对应的数据库表取消ID主键自增设置,否则报错。
@TableName("tbl_user")
public class User {
//设置INPUT策略
@TableId(type = IdType.INPUT)
private Long id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}
////////////////////
@Test
public void testSave(){
User user = new User();
//需要主动设置主键ID的值。否则报错。
user.setId(6666L);
user.setName("xiaoming");
user.setPassword("itheima");
user.setAge(12);
user.setTel("4006184000");
userDao.insert(user);
}
ASSIGN_ID策略
ASSIGN_ID策略:通过雪花算法自动生成id(可兼容数值型与字符串型)
- 使用INPUT策略的时候要确保对应的数据库表取消ID主键自增设置,否则报错。
- 注意:这种生成策略,不需要手动设置ID,如果手动设置ID,则会使用自己设置的值。
@TableName("tbl_user")
public class User {
//设置ASSIGN_ID策略
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}
////////////////////
@Test
void testSave(){
User user = new User();
user.setName("小明");
user.setPassword("itheima");
user.setAge(12);
user.setTel("4006184000");
userDao.insert(user);
}
ASSIGN_UUID策略
ASSIGN_UUID策略:以UUID生成算法作为id生成策略。
- 使用ASSIGN_UUID策略注意的是,主键的类型不能是Long,而应该改成String类型。
- 主键对应的数据库表主键字段需要设置为varchar,长度要大于32,因为UUID生成的主键为32位,如果长度小的话就会导致插入失败。
@TableName("tbl_user")
public class User {
//设置为ASSIGN_UUID策略
//需要把主键的类型改为String类型。
@TableId(type = IdType.ASSIGN_UUID)
private String id;
private String name;
@TableField(value="pwd",select=false)
private String password;
private Integer age;
private String tel;
@TableField(exist=false)
private Integer online;
}
////////////////////
@Test
void testSave(){
User user = new User();
user.setName("小明");
user.setPassword("itheima");
user.setAge(12);
user.setTel("4006184000");
userDao.insert(user);
}
小结 @TableId注解
- NONE: 不设置id生成策略,MP不自动生成,约等于INPUT,所以这两种方式都需要用户手动设置,但是手动设置第一个问题是容易出现相同的ID造成主键冲突,为了保证主键不冲突就需要做很多判定,实现起来比较复杂。
- AUTO:数据库ID自增,这种策略适合在数据库服务器只有1台的情况下使用,多台数据库服务器的情况下,容易导致id重复。
- ASSIGN_UUID:可以在分布式的情况下使用,而且能够保证唯一,但是生成的主键是32位的字符串,长度过长占用空间而且还不能排序,查询性能也慢
- ASSIGN_ID:可以在分布式的情况下使用,生成的是Long类型的数字,可以排序性能也高,但是生成的策略和服务器时间有关,如果修改了系统时间就有可能导致出现重复主键
- 综上所述,每一种主键策略都有自己的优缺点,根据自己项目业务的实际情况来选择使用才是最明智的选择。
@TableId注解 类型:属性注解 位置:模型类中用于表示主键的属性上方定义 作用:设置当前类中主键属性的生成策略
6 逻辑删除
- 物理删除:业务数据从数据库中删除,执行的是delete操作。
- 逻辑删除:为数据设置是否可用状态字段,删除时设置状态字段为不可用状态,数据保留在数据库中,执行的是update操作。
① 步骤1:给数据库表添加deleted列字段
字段名可以任意,内容也可以自定义,比如0代表正常,1代表删除,可以在添加列的同时设置其默认值为0正常。
② 步骤2:给实体类添加deleted属性
(1)添加与数据库表的列对应的一个属性名,名称可以任意,如果和数据表列名对不上,可以使用@TableField进行关系映射,如果一致,则会自动对应。 (2)标识新增的字段为逻辑删除字段,使用@TableLogic
@TableName("tbl_user")
public class User {
@TableId(type = IdType.ASSIGN_UUID)
private String id;
private String name;
private String password;
private Integer age;
private String tel;
//可用状态属性
private Integer deleted;
}
③ 步骤3:运行删除方法
@Test
public void testDelete(){
//删除数据,内部实现上是更新操作
userDao.deleteById(1L);
}
逻辑删除的本质其实是修改操作。如果加了逻辑删除字段,查询数据时也要自动带上逻辑删除字段。
mybatisplus中手写SQL
由于mybatisplus底层是封装的mybatis。因此mybatisplus中手写SQL,本质上是调用mybatis。
环境:springboot + mybatisplus
- 先配置mapper.xml文件的位置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml # mybatis的手写 sql xml文件的位置。即resources目录中
在resources目录中创建mapper目录,然后再创建各个xxxMapper.xml文件
mapper.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.shuyx.shuyxuser.mapper.UserMapper">
<!--查询用户列表,并且关联查询出用户所属的组织机构信息-->
<select id="selectUserOrgList" resultMap="UserDTO" parameterType="com.shuyx.shuyxuser.dto.UserDTO">
select u.user_id, u.org_id, u.user_name, u.email, u.avatar, u.phone,u.birthday,
u.gender, u.status, u.create_time, o.org_name
from t_user u
left join t_org o on u.org_id = o.org_id
</select>
<!--UserDTO 结果映射集合-->
<resultMap id="UserDTO" type="com.shuyx.shuyxuser.dto.UserDTO">
<!--将sql语句查出来的的字段赋值到对应的对象属性上。 -->
<id property="userId" column="user_id"/>
<result property="userName" column="user_name"/>
<result property="orgId" column="org_id"/>
<result property="email" column="email"/>
<result property="phone" column="phone"/>
<result property="gender" column="gender"/>
<result property="avatar" column="avatar"/>
<result property="password" column="password"/>
<result property="status" column="status"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<!-- 使用<association>标签将一对一关联查询的数据,赋值到指定的结果集中-->
<association property="org" javaType="com.shuyx.shuyxuser.entity.OrgEntity">
<id property="orgId" column="org_id"/>
<result property="parentId" column="parent_id"/>
<result property="orgName" column="org_name"/>
<result property="orgPath" column="org_path"/>
<result property="status" column="status"/>
</association>
</resultMap>
</mapper>
- mapper.xml文件对应的mapper接口
@Repository
public interface UserMapper extends BaseMapper<UserEntity> {
/**
* 分页查询用户信息
* @param dto
* @return
*/
public List<UserDTO> selectUserOrgList(UserDTO dto);
}
- 测试
private UserMapper userMapper
@Test
public void test1(dto){
//开始SQL查询
List<Student> list = userMapper.selectUserOrgList(dto);
System.out.println(list);
}