概述
MyBatis-Plus(简称MP)是一个基于MyBatis框架的增强工具,它在MyBatis的基础上只做增强而不做改变,旨在简化开发、提高效率。MP提供了一系列的功能和特性,使得开发人员能够更加高效地使用MyBatis进行数据库操作
官网:MyBatis-Plus
快速入门
环境搭建:先利用Tlias系统进行测试,可以从中州养老项目中给出的资料获取项目代码
导入依赖
导入MyBatis-Plus的起步依赖,替换掉MyBatis的起步依赖
1 2 3 4 5
| <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.11</version> </dependency>
|
接着,在引导类上添加注解,配置自动扫描Mapper
1 2 3 4 5 6 7 8
| @MapperScan("com.itheima.mapper") @ServletComponentScan @SpringBootApplication public class TliasWebManagementApplication { public static void main(String[] args) { SpringApplication.run(TliasWebManagementApplication.class, args); } }
|
定义Mapper
为了简化单表CRUD,Mybatis-Plus提供了一个基础的BaseMapper接口,其中已经实现了单表的CRUD:

因此咱们自定义的Mapper只要实现了这个BaseMapper,就无需自己实现单表CRUD了。
修改DeptProjectMapper接口,让其继承BaseMapper,接口中的代码可以全部注释掉或者删除:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Mapper public interface DeptMapper extends BaseMapper<Dept> {
}
|
同时可以把对应的xml映射文件改个名字或删除(彻底失效)
由于DeptMapper中进行了修改,所以还需要将对应Service层的代码进行修改:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| @Service public class DeptServiceImpl implements DeptService { @Autowired private DeptMapper deptMapper;
@Override public List<Dept> findAll() { return deptMapper.selectList(null); }
@Override public void deleteById(Integer id) { deptMapper.deleteById(id); }
@Override public void save(Dept dept) { dept.setCreateTime(LocalDateTime.now()); dept.setUpdateTime(LocalDateTime.now());
deptMapper.insert(dept); }
@Override public Dept getById(Integer id) { Dept dept = deptMapper.selectById(id); return dept; }
@Override public void update(Dept dept) { dept.setUpdateTime(LocalDateTime.now()); deptMapper.updateById(dept); } }
|
最后,修改Dept.java这个实体类,设置主键增长策略为自增:
1 2 3 4 5 6 7 8 9 10
| @Data @NoArgsConstructor @AllArgsConstructor public class Dept { @TableId(value = "id",type = IdType.AUTO) private Integer id; private String name; private LocalDateTime createTime; private LocalDateTime updateTime; }
|
常见注解(实体类映射数据表)
在上面的案例中,对于Mapper层来说,虽然没有编写任何SQL代码,但是MP依旧可以知道是对哪张表进行操作,主要的原因如下:
DeptMapper在继承BaseMapper时,指定了一个泛型
1 2 3 4
| @Mapper public interface DeptMapper extends BaseMapper<Dept> {
}
|
泛型中的Dept就是与数据库表对应的POJO
MP就是根据实体类的信息,通过反射来推断出表的信息,从而生成SQL的。规则如下:
- MP会把实体类的类名(驼峰转下划线)作为表名
- MP会把名为id的字段作为主键
- MP会把实体类的所有变量名(驼峰转下划线)作为表的字段名,并根据变量类型推断字段类型
但有些情况下,实体类的类名与表名不完全一致,又或者实体类中的属性名和数据表中的字段名无法完全对应,因此MP提供了一些注解便于声明表信息,常用的有下面几个注解:
- @TableName:用来指定表名
- @TableField:用来指定表中的普通字段信息
- @TableId:用来指定表中的主键字段信息
@TableName
- 描述:表名注解,标识实体类对应的表
- 使用位置:实体类
- 示例:
1 2 3 4 5 6 7 8 9 10 11
| @Data @AllArgsConstructor @NoArgsConstructor @TableName("tb_user") public class User { private Long id; private String name; private Boolean isMarried; private Integer order; private String address; }
|
@TableField
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @Data @AllArgsConstructor @NoArgsConstructor @TableName("tb_user") public class User { private Long id; @TableField("username") private String name; private Boolean isMarried; @TableField("`order`") private Integer order; @TableField(exist=false) private String address; }
|
一般情况下是不需要为成员变量添加@TableField注解,一些特殊情况除外:
- 成员变量名与数据库字段名不一致
- 成员变量与数据库一致,但是与数据库的关键字冲突。使用@TableField注解结合
``指定字段名称
- 成员变量不是数据库中的字段,则需要将
exist属性设置为false,明确告知MP该字段在数据表中不存在
@TableId
- 描述:主键注解,标识实体类中的主键字段和主键生成类型
- 使用位置:实体类的主键字段
- 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Data @NoArgsConstructor @AllArgsConstructor @TableName("tb_user") public class User { @TableId(value = "id", type = IdType.AUTO) private Long id;
private String name;
private Boolean isMarried;
private Integer order;
private String address; }
|
value:指定数据库表中的主键字段名称
IdType:指定主键生成类型,支持的类型有:
- **
IdType.AUTO**:使用数据库自增 ID 作为主键。
IdType.NONE:无特定生成策略,如果全局配置中有 IdType 相关的配置,则会跟随全局配置。
IdType.INPUT:在插入数据前,由用户自行设置主键值。
- **
IdType.ASSIGN_ID**:自动分配 ID,适用于 Long、Integer、String 类型的主键。
默认使用雪花算法通过 IdentifierGenerator 的 nextId 实现,这是默认的ID策略。
IdType.ASSIGN_UUID:自动分配 UUID,适用于 String 类型的主键。默认实现为 IdentifierGenerator 的 nextUUID 方法。
常见配置
Mybatis-Plus也支持基于yml文件的自定义配置,详见官方文档:使用配置 | MyBatis-Plus
常见的配置:
1 2 3 4 5
| mybatis-plus: global-config: db-config: id-type: auto update-strategy: not_null
|
Mybatis-Plus也支持手写SQL的,而mapper层的xml映射文件的读取位置可以自己配置:
type-aliases-package:指定 MyBatis 别名包扫描路径,用于给包中的类注册别名。注册后,在 Mapper 对应的 XML 文件中可以直接使用类名,无需使用全限定类名
mapper-locations:指定 MyBatis 的 Mapper 层 XML 映射文件的位置,默认值:["classpath*:/mapper/**/*.xml"]
1 2 3 4 5 6 7 8 9 10 11
| mybatis-plus: type-aliases-package: com.itheima.pojo mapper-locations: classpath:mapper/**/*Mapper.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config: db-config: id-type: auto update-strategy: not_null
|
mapper-locations也可以配置成classpath*:mapper/**.xml,也就是说只要把mapper.xml文件放置在mapper目录下就一定会被加载。
通过以上的配置信息,可以将DeptMapper.xml中的实体类resultType属性的包名省略:
1 2 3 4 5 6 7 8 9
| <?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.itheima.mapper.DeptMapper"> <select id="getById" resultType="Dept"> select id, name, create_time, update_time from dept where id = #{id} </select> </mapper>
|
核心功能
刚才的案例中都是以id为条件的简单CRUD,一些复杂条件的SQL语句就要用到一些更高级的功能了,Mybatis-Plus中的核心功能,包括两部分内容:
条件构建器
除了查询以外,修改、删除的SQL语句都需要指定where条件。因此BaseMapper中不仅仅提供了接收id作为where条件的方法,还提供了一组可以接收更加复杂的where条件的方法

参数中的Wrapper就是条件构建的抽象类,其下有很多默认实现,继承关系如图:

Wrapper的子类AbstractWrapper提供了where中包含的所有类型条件的构建方法:

而QueryWrapper在AbstractWrapper的基础上拓展了一个select方法,允许指定查询字段:

而UpdateWrapper在AbstractWrapper的基础上拓展了一个set方法,允许指定SQL中的SET部分:

QueryWrapper
无论是查询、修改、删除,都可以使用QueryWrapper来构建条件
示例1:
需求:查询姓名中包含“李”且薪资大于等于5000的员工的 id, name, phone, salary字段
SQL语句:select id, name, phone, salary from emp where name like '%李%' and salary >= 5000;
实现代码如下:
首先让EmpMapper继承自BaseMapper:
1 2 3 4
| @Mapper public interface EmpMapper extends BaseMapper<Emp> {
}
|
其次,在测试类中注入EmpMapper,并创建条件构建器,最后使用BaseMapper的selectList方法,并传入构建器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @SpringBootTest public class TestQueryWrapper {
@Autowired private EmpMapper empMapper;
@Test public void testQueryWrapper() { QueryWrapper<Emp> queryWrapper = new QueryWrapper<>(); queryWrapper.like("name", "李") .ge("salary", 5000) .select("id", "name", "phone", "salary");
List<Emp> emps = empMapper.selectList(queryWrapper); emps.forEach(System.out::println); } }
|
示例2:
需求:更新名为”李忠”的员工的薪水为9000
实现代码如下:
第一步和示例1一样,这里省略
依旧是注入EmpMapper,然后创建条件构建器,并使用BaseMapper的update方法,传入构建器:
1 2 3 4 5 6 7 8 9 10 11 12
| @Autowired private EmpMapper empMapper;
@Test public void testUpdateByQueryWrapper() { Emp emp = new Emp(); emp.setSalary(9000); QueryWrapper<Emp> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("name", "李忠"); empMapper.update(emp, queryWrapper); }
|
UpdateWrapper
示例:
需求:更新id为5, 6, 7的员工的薪水,加2000
SQL语句:update emp set salary = salary + 2000 where id in (5, 6, 7);
实现代码如下:
EmpMapper继承BaseMapper,测试类注入EmpMapper,这两步还是和QueryWrapper中的示例一样
具体测试方法如下:
1 2 3 4 5 6 7 8
| @Test public void testUpdateWrapper() { UpdateWrapper<Emp> updateWrapper = new UpdateWrapper<>(); updateWrapper.in("id", 5, 6, 7) .setSql("salary = salary + 2000"); empMapper.update(updateWrapper); }
|
setSql中的参数就是SQL语句里set后面跟随的更新内容
LambdaQueryWrapper
无论是QueryWrapper还是UpdateWrapper在构造条件的时候都需要写死字段名称,这在编程规范中显然是不推荐的
那怎么样才能不写字段名,又能知道字段名呢?
其中一种办法是基于变量的**getter**方法结合反射技术来实现,因此咱们只要将条件对应的字段的getter方法传递给Mybatis-Plus,它就能计算出对应的变量名了。而传递方法可以使用JDK8中的方法引用和Lambda表达式。 因此Mybatis-Plus又提供了一套基于Lambda的Wrapper,包含两个类:
- LambdaQueryWrapper
- LambdaUpdateWrapper
分别对应QueryWrapper和UpdateWrapper
其使用方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Test public void testLambdaQueryWrapper() { LambdaQueryWrapper<Emp> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.like(Emp::getName, "李") .gt(Emp::getSalary, 5000) .select(Emp::getId, Emp::getName, Emp::getPhone, Emp::getSalary);
List<Emp> emps = empMapper.selectList(queryWrapper); System.out.println(emps); }
@Test public void testLambdaUpdateWrapper() { LambdaUpdateWrapper<Emp> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.in(Emp::getId, 5, 6, 7) .setSql("salary = salary + 2000"); empMapper.update(updateWrapper); }
|
IService
MP不仅提供了Map层接口BaseMapper,还提供了通用的Service层接口以及默认实现,并在其中封装了一些常用的service模板方法。通用接口为IService,默认实现为ServiceImpl,其中封装的方法可以分为以下几类:
save:新增
remove:删除
update:更新
get:查询单个结果
list:查询集合结果
count:计数
page:分页查询
基本增删改查
新增:

- **
save**:新增单个元素
- **
saveBatch**:批量新增
saveOrUpdate是根据id判断,如果数据存在就更新,不存在则新增
saveOrUpdateBatch是批量的新增或修改
删除:

- **
removeById**:根据id删除
removeByIds:根据id批量删除
removeByMap:根据Map中的键值对条件删除
remove(Wrapper<T>):根据Wrapper条件删除
修改:

- **
updateById**:根据id修改
update(Wrapper<T>):根据UpdateWrapper修改,Wrapper中包含set和where部分
update(T,Wrapper<T>):按照T内的数据修改与Wrapper匹配到的数据
updateBatchById:根据id批量修改
查询单个数据:

getById:根据id查询1条数据
getOne(Wrapper<T>):根据Wrapper查询1条数据
getBaseMapper:获取Service内的BaseMapper实现,某些时候需要直接调用Mapper内的自定义SQL时可以用这个方法获取到Mapper
查询多条数据:

listByIds:根据id批量查询
list(Wrapper<T>):根据Wrapper条件查询多条数据
list():查询所有
统计数据:

count():统计所有数量
count(Wrapper<T>):统计符合Wrapper条件的数据数量
基本用法与快速入门

由于Service中经常需要定义与业务有关的自定义方法,因此咱们不能直接使用IService,而是自定义Service接口,然后继承IService以拓展方法。同时,让自定义的Service实现类继承ServiceImpl,这样就不用自己实现IService中的接口了
需求:基于Mybatis-Plus的 Iservice 接口,实现Tlias智能学习辅助系统部门管理页面的所有功能:
- 新增部门
- 根据id查询部门
- 根据id更新部门
- 根据id删除部门
首先,找到DeptService接口,让它继承IService接口,并在泛型上指定实体类的类型:
1 2 3
| public interface DeptService extends IService<Dept> {
}
|
改完之后,原来书写的方法可以全部删除
接着,找到DeptService接口的实现类:DeptServiceImpl,让它继承自ServiceImpl类,在泛型的位置指定Mapper层接口和实体类:
1 2 3 4
| @Service public class DeptServiceImpl extends ServiceImpl<DeptMapper, Dept> implements DeptService {
}
|
最后,修改DeptController中报错的代码,改一下方法调用就可以正常运行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| @Slf4j @RequestMapping("/depts") @RestController public class DeptController { @Autowired private DeptService deptService;
@GetMapping public Result findAll() { log.info("查询所有部门数据");
List<Dept> depts = deptService.list();
return Result.success(depts); }
@DeleteMapping public Result deleteById(Integer id) { log.info("根据id删除部门,删除的部门id:{}", id); deptService.removeById(id); return Result.success(); }
@PostMapping public Result save(@RequestBody Dept dept) { log.info("新增部门,要新增的部门信息:{}", dept); deptService.save(dept); return Result.success(); }
@GetMapping("/{id}") public Result getById(@PathVariable Integer id) { log.info("根据id查询部门,要查询的id:{}", id);
Dept dept = deptService.getById(id);
return Result.success(dept); }
@PutMapping public Result update(@RequestBody Dept dept) { log.info("修改部门数据,修改后的部门数据:{}", dept); deptService.updateById(dept); return Result.success(); } }
|
实战
准备工作
Tlias员工管理部分,由MyBatis升级为MyBatis-Plus
由于员工管理中存在分页条件查询的功能,所以还需要使用到MP中的分页功能(PaginationInnerInterceptor)
于 v3.5.9 起,PaginationInnerInterceptor 已分离出来。如需使用,则需单独引入 mybatis-plus-jsqlparser 依赖
此时,由于项目中引入的MP依赖为3.5.11版本:
1 2 3 4 5
| <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.11</version> </dependency>
|
所以还需要单独引入mybatis-plus-jsqlparser:
1 2 3 4 5
| <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-jsqlparser</artifactId> <version>3.5.11</version> </dependency>
|
接下来,在项目中新建一个配置类:com.itheima.config.MybatisConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| package com.itheima.config;
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration;
@Configuration public class MybatisConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); paginationInnerInterceptor.setMaxLimit(1000L); mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);
return mybatisPlusInterceptor; } }
|
最后一步,就是业务层的继承和实现关系:
- 首先先将EmpMapper继承BaseMapper:
1 2 3 4
| @Mapper public interface StudentMapper {
}
|
- 其次,再对Service的Emp进行修改,先是EmpService接口,需要继承IService接口:
1 2 3
| public interface EmpService extends IService<Emp> {
}
|
- 然后对其实现类增加ServiceImpl的继承:
1 2 3 4
| @Service public class EmpServiceImpl extends ServiceImpl<EmpMapper, Emp> implements EmpService {
}
|
分页查询修改
在控制层中编写分页查询的接口:
1 2 3 4 5 6 7 8 9 10
| @GetMapping public Result page(EmpQueryParam param) { log.info("员工列表查询条件:{}", param); PageResult<Emp> pageResult = empService.getPageResult(param); return Result.success(pageResult); }
|
业务层代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| @Autowired private DeptService deptService;
@Override public PageResult<Emp> getPageResult(EmpQueryParam param) { Page<Emp> pageParam = Page.of(param.getPage(), param.getPageSize()); pageParam.addOrder(OrderItem.desc("update_time")); Page<Emp> pageResult = page(pageParam); List<Emp> emps = page.getRecords(); List<Integer> deptIds = emps.stream().map(Emp::getDeptId).collect(Collectors.toList()); if (!deptIds.isEmpty()) { List<Dept> depts = deptService.listByIds(deptIds); emps.forEach(emp -> { depts.forEach(dept -> { if (emp.getDeptId() != null && emp.getDeptId().equals(dept.getId())) { emp.setDeptName(dept.getName()); } }); }); } return new PageResult<>(page.getTotal(), emps); }
|
原本的分页查询中,SQL语句是多表查询,但是MP不支持多表查询,所以可以拆分成两次单表查询
第一次查询emp表,将所有员工数据中的dept_id保存到集合中
第二次查询dept表,根据dept_id获取对应的部门名称,将部门名称再封装到Emp对象中
分页条件查询
page方法不仅可以传入分页参数,还可以传入构建器wrapper
1
| page(E page, Wrapper<T> queryWrapper)
|
所以,在原有代码中,加入构建器并作为参数传入page就能实现分页条件查询:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| @Override public PageResult<Emp> getPageResult(EmpQueryParam param) { Page<Emp> pageParam = Page.of(param.getPage(), param.getPageSize());
pageParam.addOrder(OrderItem.desc("update_time"));
LambdaQueryWrapper<Emp> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper .like(param.getName() != null && !param.getName().isEmpty(), Emp::getName, param.getName()) .eq(param.getGender() != null, Emp::getGender, param.getGender()) .between(param.getBegin() != null && param.getEnd() != null, Emp::getEntryDate, param.getBegin(), param.getEnd());
Page<Emp> pageResult = page(pageParam, queryWrapper);
List<Emp> emps = pageResult.getRecords();
List<Integer> deptIds = emps.stream().map(Emp::getDeptId).collect(Collectors.toList());
if (!deptIds.isEmpty()) { List<Dept> depts = deptService.listByIds(deptIds);
emps.forEach(emp -> { depts.forEach(dept -> { if (emp.getDeptId() != null && emp.getDeptId().equals(dept.getId())) { emp.setDeptName(dept.getName()); } }); }); }
return new PageResult<>(pageResult.getTotal(), emps); }
|