《SpringBoot 3:入门与应用实战》第 13 章 整合 MyBatis 使用 MyBatis-Plus 阅读笔记 40
13.5 使用 MyBatis-Plus
MyBatis-Plus 作为 MyBatis 的增强框架,官方给出一个比较有趣的说法:MyBatis 与 MyBatis-Plus 好比《魂斗罗》里的比尔与兰斯,两者搭配效率翻倍。MyBatis-Plus 提供了强大的支撑,构筑了一些实用的特性,本节会列举一些日常使用频率比较高的特性,关于详细的功能特性使用可以参照 MyBatis-Plus 的官方文档。
13.5.1 CRUD 基础接口
MyBatis-Plus 提供的重要基础能力之一就是单表 CRUD 接口,由于 BaseMapper 接口中预先定义的方法足够多,使得在编写具体的业务模块时,单表的 CRUD 几乎不需要编写,继承 BaseMapper 接口后 Mapper 接口就可以自动拥有单表 CRUD 的能力。DepartmentMapper 继承了BaseMapper 后,它已经拥有了包含 insert、update、delete、selectOne、selectList、count 等操作单表数据的能力。

MyBatis-Plus 的 BaseMapper<T> 接口是其核心组件,封装了单表常用的 CRUD 操作。以下是 BaseMapper 中常用方法的分类表格说明:
📥 新增操作 (Insert)
| 方法签名 | 说明 |
|---|---|
int insert(T entity) |
插入一条记录。返回受影响的行数。如果实体类主键未赋值,MP 会根据配置的主键策略(如雪花算法)自动生成并回写到实体对象中。 |
🗑️ 删除操作 (Delete)
| 方法签名 | 说明 |
|---|---|
int deleteById(Serializable id) |
根据主键 ID 删除一条记录。 |
int deleteBatchIds(Collection<? extends Serializable> idList) |
根据主键 ID 列表批量删除多条记录。 |
int deleteByMap(Map<String, Object> columnMap) |
根据 Map 中的键值对作为条件(等值匹配)删除记录。 |
int delete(Wrapper<T> wrapper) |
根据条件构造器(Wrapper)删除记录。如果不传 Wrapper 或 Wrapper 为空,会删除全表数据(需配合防全表删除插件使用)。 |
✏️ 修改操作 (Update)
| 方法签名 | 说明 |
|---|---|
int updateById(T entity) |
根据主键 ID 更新记录。注意 :默认策略下,实体类中为 null 的字段不会被更新到数据库。 |
int update(T entity, Wrapper<T> updateWrapper) |
根据条件构造器更新记录。entity 用于设置要更新的字段值,updateWrapper 用于设置 WHERE 条件。若 entity 为 null,则需通过 updateWrapper.set() 方法指定更新字段。 |
🔍 查询操作 (Select)
| 方法签名 | 说明 |
|---|---|
T selectById(Serializable id) |
根据主键 ID 查询单条记录。 |
List<T> selectBatchIds(Collection<? extends Serializable> idList) |
根据主键 ID 列表批量查询多条记录。 |
T selectOne(Wrapper<T> queryWrapper) |
根据条件构造器查询单条记录。如果查询结果多于一条,会抛出异常。 |
List<T> selectList(Wrapper<T> queryWrapper) |
根据条件构造器查询列表。如果 queryWrapper 为 null,则查询全表数据。 |
List<Map<String, Object>> selectMaps(Wrapper<T> queryWrapper) |
根据条件构造器查询列表,但结果封装为 Map。适用于只查询部分字段或进行统计查询的场景。 |
List<Object> selectObjs(Wrapper<T> queryWrapper) |
根据条件构造器查询列表,但只返回结果集中每一行的第一个字段值。 |
Integer selectCount(Wrapper<T> queryWrapper) |
根据条件构造器查询总记录数。 |
IPage<T> selectPage(IPage<T> page, Wrapper<T> queryWrapper) |
根据条件构造器进行分页查询。需要配合分页插件使用。 |
IPage<Map<String, Object>> selectMapsPage(IPage<T> page, Wrapper<T> queryWrapper) |
根据条件构造器进行分页查询,结果封装为 Map。 |
💡 核心要点补充
- 条件构造器 (Wrapper) :
BaseMapper中大部分高级查询和更新方法都依赖Wrapper条件构造器(如QueryWrapper、LambdaQueryWrapper)。它允许你以面向对象的方式链式构建 SQL 的 WHERE 条件、ORDER BY 排序等。 - 主键策略 :
insert方法会自动处理主键生成。默认使用雪花算法(ASSIGN_ID),也可以在实体类上通过@TableId(type = IdType.AUTO)指定为数据库自增。 - 逻辑删除 :如果配置了逻辑删除(
@TableLogic),deleteById等方法会自动转换为UPDATE语句,将逻辑删除字段的值置为已删除状态,而不是物理删除数据。 - 字段自动填充 :配合
@TableField(fill = FieldFill.INSERT)等注解和MetaObjectHandler实现类,可以在插入或更新时自动填充创建时间、更新时间等字段。
13.5.2 Wrapper 机制
Wrapper 是 MyBatis-Plus 编程式查询、修改数据的重要特性。MyBatis-Plus 提供的 Wrapper 机制拥有对单表查询的灵活条件构造、投影查询、聚合查询等能力,而且根据操作的场景分为查询类 Wrapper 和更新类 Wrapper,使用方式基本一致。下面通过几个简单示例来了解查询类Wrapper 的使用。
1.简单使用
先通过一个简单的模糊查询来演示 Wrapper 的基本使用。代码使用 Wrappers 的静态方法 query,构造一个 Department 类的条件查询器,并模糊查询 name 属性包含"分公司"的数据。随后调用这个 /dept/test3 接口,浏览器中可以成功得到一条数据,证明条件查询已经生效。


2.Lambda 式使用
对于代码中 Wrapper 的使用,存在一个问题:当 Department 类的属性发生变化时,Wrapper 中的条件构造也需要相应地修改,但是 IDE 并不会感知到这些属性被使用。为了更易维护可能变化的实体类属性,MyBatis-Plus 提供了 LambdaWrapper,使用这种类型的 Wrapper 将属性的字符串变量改为 Lambda 表达式,以此实现代码的高可维护性。代码使用 lambdaQuery 方法,可以生成一个传入 Lambda 表达式的LambdaQueryWrapper,效果完全一致。

java
package com.yangjunbo.springboot.mybatisplus;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/dept")
public class DepartmentController {
@Autowired
private DepartmentService departmentService;
@GetMapping("/test1")
public List<Department> test1() {
return departmentService.list();
}
@GetMapping("/test2")
public Long test2() {
return departmentService.count();
}
@GetMapping("/test3")
public List<Department> test3() {
return departmentService.list(Wrappers.<Department>query().like("name", "分公司"));
}
@GetMapping("/test4")
public List<Department> test4() {
return departmentService.list(Wrappers.lambdaQuery(Department.class)
.like(Department::getName, "分公司"));
}
}

3.投影查询
如果一个表的列特别多,而只需要查询其中的几列数据时,可以使用投影查询,即通过指定需要查询的列来达到节省数据库流量带宽的目的。代码中指定了 Department 类的查询结果中只提取 id 和 name,其他属性都不需要。编写完成后重新编译代码,并在浏览器中访问 /dept/test5 接口,发现返回的数据中 pid 和 del 属性都为 null,达到了预期目标。

java
package com.yangjunbo.springboot.mybatisplus;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/dept")
public class DepartmentController {
@Autowired
private DepartmentService departmentService;
@GetMapping("/test1")
public List<Department> test1() {
return departmentService.list();
}
@GetMapping("/test2")
public Long test2() {
return departmentService.count();
}
@GetMapping("/test3")
public List<Department> test3() {
return departmentService.list(Wrappers.<Department>query().like("name", "分公司"));
}
@GetMapping("/test4")
public List<Department> test4() {
return departmentService.list(Wrappers.lambdaQuery(Department.class)
.like(Department::getName, "分公司"));
}
@GetMapping("/test5")
public List<Department> test5() {
return departmentService.list(Wrappers.lambdaQuery(Department.class).select(Department::getId, Department::getName));
}
}

4.聚合查询
对于单表查询来讲,聚合查询也是一个常见的查询场景。虽然 MyBatis-Plus 没有对聚合函数提供 API 的定义,但是可以传入 SQL 片段来间接实现聚合查询。代码是一个查询 tbl_dept 表中最大 id 值的例子。需要注意的是,在使用聚合函数查询时无法使用 LambdaQueryWrapper 来实现,而只能使用普通的 QueryWrapper,也正是由于只能传入 SQL 片段进行查询,因此它的弊端与 1.简单使用 是一样的,都要考虑后期数据库列名发生变动所带来的影响。

java
package com.yangjunbo.springboot.mybatisplus;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/dept")
public class DepartmentController {
@Autowired
private DepartmentService departmentService;
@GetMapping("/test1")
public List<Department> test1() {
return departmentService.list();
}
@GetMapping("/test2")
public Long test2() {
return departmentService.count();
}
@GetMapping("/test3")
public List<Department> test3() {
return departmentService.list(Wrappers.<Department>query().like("name", "分公司"));
}
@GetMapping("/test4")
public List<Department> test4() {
return departmentService.list(Wrappers.lambdaQuery(Department.class)
.like(Department::getName, "分公司"));
}
@GetMapping("/test5")
public List<Department> test5() {
return departmentService.list(Wrappers.lambdaQuery(Department.class).select(Department::getId, Department::getName));
}
@GetMapping("/test6")
public Department test6() {
return departmentService.getOne(Wrappers.query(Department.class).select("max(id) as id"));
}
}

5.更灵活的条件构造
在实际的项目开发中,可能存在一些条件构造的场景:只有满足 ××× 条件后,才构造 Wrapper 中的查询条件。代码中展示了构造 Wrapper 条件的两种条件判断,传统的条件判断需要使用 if 结构设计,而 Wrapper 的方法中提供了一系列扩展方法,这些方法的共性是第一个参数传入一个boolean 类型的值,当这个 boolean 值为 true 时当前构造的查询条件才会生效。换句话说,上下两种写法是等价的。

java
package com.yangjunbo.springboot.mybatisplus;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@RequestMapping("/dept")
public class DepartmentController {
@Autowired
private DepartmentService departmentService;
@GetMapping("/test1")
public List<Department> test1() {
return departmentService.list();
}
@GetMapping("/test2")
public Long test2() {
return departmentService.count();
}
@GetMapping("/test3")
public List<Department> test3() {
return departmentService.list(Wrappers.<Department>query().like("name", "分公司"));
}
@GetMapping("/test4")
public List<Department> test4() {
return departmentService.list(Wrappers.lambdaQuery(Department.class)
.like(Department::getName, "分公司"));
}
@GetMapping("/test5")
public List<Department> test5() {
return departmentService.list(Wrappers.lambdaQuery(Department.class).select(Department::getId, Department::getName));
}
@GetMapping("/test6")
public Department test6() {
return departmentService.getOne(Wrappers.query(Department.class).select("max(id) as id"));
}
@GetMapping("/test7")
public List<Department> test7(Department qo) {
LambdaQueryWrapper<Department> wrapper = Wrappers.lambdaQuery(Department.class);
if (qo.getId() != null) {
wrapper.eq(Department::getId, qo.getId());
}
wrapper.like(StringUtils.hasText(qo.getName()), Department::getName, qo.getName());
return departmentService.list(wrapper);
}
}
13.5.3 主键策略与 ID 生成器
考虑到在项目开发中可能会用到的几种主键类型,MyBatis-Plus 给予了一些基础实现和配置。
- AUTO:数据库主键自增。
- ASSIGN_ID:雪花算法 ID。
- ASSIGN_UUID:不带短横线的 UUID。
- INPUT:程序手动设置的 ID(或配合序列填充,Oracle、SQL Server 等使用)。
- NONE:逻辑主键,数据库表中没有定义主键。
默认情况下,MyBatis-Plus 使用的主键策略是使用了雪花算法的 ASSIGN_ID 策略。
13.5.4 逻辑删除
逻辑删除是代替 DML 语句中 delete(物理删除)的一种更适合项目开发的数据删除机制,通过设置一个特殊的标记位,将需要删除的数据设置为 "不可见",并在每次查询数据时只查询标记位数据值为 "可见" 的数据,这样的设计就是逻辑删除。MyBatis-Plus 实现逻辑删除非常简单,只需要两步。
1.application.yaml 中配置
MyBatis-Plus 在全局配置文件中提供了 3 个配置属性,分别是全局逻辑删除字段名、逻辑删除的数据标记值、没有逻辑删除的数据标记值。
yaml
mybatis-plus.global-config.db-config.logic-delete-field=del
mybatis-plus.global-config.db-config.logic-delete-value=1
mybatis-plus.global-config.db-config.logic-not-delete-value=0
2.在实体类上标注 @TableLogic
在具体的实体类中,只需要在逻辑删除的属性上标注 @TableLogic,即可完成逻辑删除的使用示。如此标注后实现的效果主要有两个:(1) 在使用 QueryWrapper 查询数据时,发送到数据库的 SQL 中会追加一个条件,在本示例中追加的条件为 del=0;(2) 在使用 delete 系列方法删除数据时,发送的 SQL 会由原来的 delete from xxx where id=xxx 改为 update xxx set del=1 where id=xxx。
java
@Data
@TableName("tbl_dept")
public class Department {
@TableId(type = IdType.AUTO)
private Integer id;
// ......
@TableLogic
private Boolean del;
}
13.5.5 乐观锁插件
乐观锁是高并发下的控制手段,它假设多用户并发事务在处理时不会互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。换句话说,乐观锁希望一条即将被更新的数据没有被其他用户操作过。乐观锁的实现方式如下:
- 给数据添加 version 属性;
- 当查询数据时,把 version 数据一并输出;
- 更新数据时,将查询的 version 数据值一并传入;
- 执行 update/delete 语句时,在 where 条件中额外添加 version=? 语句;
- 如果 version 数据与数据库中的不一致,则更新/删除失败。
1.注册乐观锁插件
乐观锁在 MyBatis-Plus 中的设计以插件 (Interceptor) 的形式实现,所以需要声明一个配置类,在其中注册一个 MyBatis-Plus 的插件OptimisticLockerInnerInterceptor。需要注意的是,MyBatis-Plus 提供的插件大多需要集成到 MybatisPlusInterceptor 中组合实现,而不是一个个地直接注册到 IOC 容器中。
java
@Configuration(proxyBeanMethods = false)
public class MyBatisPlusConfiguration {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
2.实体类上添加 @Version
随后可以在实体类中声明一个 Integer 类型的字段 version,并标注 @Version。如此编写完毕后,在操作 tbl_dept 表的单表数据时,乐观锁就会介入处理。
java
@Data
@TableName("tbl_dept")
public class Department {
@TableId(type = IdType.AUTO)
private Integer id;
@Version // 此处使用乐观锁注解
private Integer version;
// ......
}
MyBatis-Plus 支持的乐观锁适用于以下数据类型:
int、long(基本数据类型的数值型);
Integer、Long(包装数据类型的数值型);
Date、Timestamp、LocalDateTime(时间戳型)。
13.5.6 分页插件
分页查询是项目开发中非常常见的业务场景,对于 MyBatis 的分页插件而言,比较常见的是 PageHelper,MyBatis-Plus 已经考虑到了分页查询的场景,它提供了一个专门用于分页的插件,通过简单的配置就可以使用分页的特性。它的使用方式分为两步。
1.添加分页插件
13.5.5 节中提到 MyBatis-Plus 的插件大多需要集中组合到 MybatisPlusInterceptor 中,所以在之前代码的基础上再注册一个PaginationInnerInterceptor 即可。注意,每个数据库的分页语法不尽相同,所以在创建分页插件时需要指定数据库的方言。
java
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(new MySqlDialect()));
return interceptor;
}
2.使用分页模型
MyBatis-Plus 的 ServiceImpl 中已经提供了一些单表查询的分页方法,它们的名称都以 page 开头,可以直接使用;除此之外编写的 SQL 语句也有需要分页的场景(如关联多表的分页查询),这种情况下编写的 Mapper 接口要遵循一定的规则,即分页查询的方法需要传入 IPage 对象,并返回 IPage 模型或 List 集合。代码提供了几个简单的示例。
java
/**
* 使用IPage作为入参和返回值
* @param query
* @return
*/
IPage<User> page(IPage<Department> query);
/**
* 使用集合作为返回值
* @param query
* @return
*/
List<User> pageList(IPage<Department> query);
/**
* 使用IPage和其他参数共同作为入参
* @param page
* @param params
* @return
*/
IPage<User> pageParams(@Param("page") IPage<Department> page, @Param("params") Map<String, Object> params);
13.5.7 代码生成器
日常开发中不乏会编写很多相似度较高的代码,包括但不限于单表的 Mapper、Service、Controller 等代码,以及其中的 CRUD 代码等。为了方便项目开发,MyBatis-Plus 提供了一个代码生成器,利用这个代码生成器可以生成实体模型类、Mapper 接口、mapper.xml、Service、Controller 等代码。
使用代码生成器时,需要在引入 MyBatis-Plus 核心包的基础上,再依赖一个 mybatis-plus-generator 以及模板引擎 FreeMarker,导入的坐标如代码所示。
xml
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.32</version>
</dependency>
之后只需要编写简单的测试代码,指定代码生成器连接的数据源、生成代码的文件位置、代码所处的根包名和模块名以及需要生成的表名,如代码所示。由于编写的是一个普通的 main 方法驱动的类,因此直接运行 main 方法即可,最终生成的代码相对简洁,读者可以根据开发项目的需要自行制作基于 FreeMarker 的代码模板,并让 MyBatis-Plus 根据模板内容生成代码。
java
public class CodeGenerator {
public static void main(String[] args) {
FastAutoGenerator.create("jdbc:mysql://localhost:3306/sring-dao?characterEncoding=utf8", "springboot", "123456")
.globalConfig(builder -> {
builder.author("LinkedBear") // 设置代码的author
.outputDir("D://codegenerator"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.linkedbear.springboot.mybatisplus") // 设置父包名
.moduleName("user") // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.xml, "D://codegenerator")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("tbl_user") // 设置需要生成的表名
.addTablePrefix("tbl_"); // 设置过滤表前缀
})
// 使用FreeMarker引擎模板,默认的是Velocity引擎模板
.templateEngine(new FreeMarkerTemplateEngine())
.execute();
}
}
13.6 小结
MyBatis 是目前主流的数据层应用框架,它基于半自动 ORM 的思想,以 SQL 为中心操纵数据库。作为 MyBatis 的增强工具,MyBatis-Plus 使基于 MyBatis 的项目开发效率更高。
MyBatis 的常用机制包括配置属性设置、Mapper 接口与 mapper.xml 的动态 SQL 编写、缓存和插件的应用等,合理灵活地使用它可以大大提升开发效果。
MyBatis-Plus 在原有 MyBatis 的基础上进行功能特性的增强,而没有对原有特性进行修改和移除。MyBatis-Plus 提供的特性大多和实际的项目开发相关,可以使用 Wrapper 机制方便地进行单表 CRUD 操作,使用逻辑删除、乐观锁等插件完成功能字段的设计,此外它还提供了分页机制和提高开发效率的代码生成器。