条件构造器

MyBatis-Plus 提供了一套强大的条件构造器(Wrapper),用于构建复杂的数据库查询条件。

Wrapper 类允许开发者以链式调用的方式构造查询条件,无需编写繁琐的 SQL 语句,从而提高开发效率并减少 SQL 注入的风险。

主要的 Wrapper 类及其功能

类名 用途 字段引用方式 核心特点
QueryWrapper 查询 (SELECT) 字符串 ("name") 灵活,但字段名写错编译不报错
UpdateWrapper 更新 (UPDATE) 字符串 ("name") 专门用于构造更新条件和 SET 值
LambdaQueryWrapper 查询 (SELECT) Lambda (User::getName) 类型安全,重构友好,防拼写错误
LambdaUpdateWrapper 更新 (UPDATE) Lambda (User::getName) 类型安全,更新操作更安全
  • QueryWrapper是最基础的查询构造器

    1
    2
    3
    4
    5
    6
    7
    8
    // 需求:查询名字包含 "张",年龄大于 20 岁,且按年龄降序排列的用户
    QueryWrapper<User> queryWrapper = new QueryWrapper<>();
    queryWrapper.like("name", "张") // name LIKE '%张%'
    .gt("age", 20) // AND age > 20
    .orderByDesc("age"); // ORDER BY age DESC

    // 执行查询
    List<User> users = userMapper.selectList(queryWrapper);
  • UpdateWrapper专门用于构建 UPDATE 语句。除了构建 WHERE 条件外,它还有set方法,用于指定要更新的字段和值

    1
    2
    3
    4
    5
    6
    7
    // 需求:将所有名字包含 "张" 的用户,邮箱修改为 "test@mp.com"
    UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
    updateWrapper.like("name", "张") // WHERE name LIKE '%张%'
    .set("email", "test@mp.com"); // SET email = 'test@mp.com'

    // 执行更新
    userMapper.update(null, updateWrapper);

    如果不使用 set 方法,MP 默认会根据你传入的实体对象中非空字段进行更新。

  • LambdaQueryWrapper,利用 Java 8 的 Lambda 表达式,通过 User::getName 这种“方法引用”来代替 "name" 字符串。

    1
    2
    3
    4
    5
    6
    // 需求:查询年龄为 20 且邮箱不为空的用户
    LambdaQueryWrapper<User> lambdaQueryWrapper = new LambdaQueryWrapper<>();
    lambdaQueryWrapper.eq(User::getAge, 20) // age = 20
    .isNotNull(User::getEmail); // email IS NOT NULL

    List<User> users = userMapper.selectList(lambdaQueryWrapper);
  • LambdaUpdateWrapper同理,在设置更新字段时,也可以使用 Lambda 表达式,避免硬编码。

    1
    2
    3
    4
    5
    6
    // 需求:将 ID 为 1 的用户年龄改为 25
    LambdaUpdateWrapper<User> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
    lambdaUpdateWrapper.eq(User::getId, 1) // WHERE id = 1
    .set(User::getAge, 25); // SET age = 25

    userMapper.update(null, lambdaUpdateWrapper);
  • 除了上面提到的 Wrapper,MP 还提供了 LambdaQueryChainWrapper,这种写法不需要 new Wrapper,直接链式调用

    1
    2
    3
    4
    5
    // 这种写法不需要 new Wrapper,直接链式调用
    List<User> list = new LambdaQueryChainWrapper<>(userMapper)
    .eq(User::getAge, 20)
    .like(User::getName, "张")
    .list(); // 最后调用 list() 或 one() 执行

而对于AbstractWrapper,这是一个抽象基类,提供了所有 Wrapper 类共有的方法和属性。它定义了条件构造的基本逻辑,包括字段(column)、值(value)、操作符(condition)等。所有的 QueryWrapperUpdateWrapperLambdaQueryWrapperLambdaUpdateWrapper 都继承自 AbstractWrapper

而且 MyBatis-Plus 提供了 Wrappers 类,它是一个静态工厂类,用于快速创建 QueryWrapperUpdateWrapperLambdaQueryWrapperLambdaUpdateWrapper 的实例。使用 Wrappers 可以减少代码量,提高开发效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建 QueryWrapper
QueryWrapper<User> queryWrapper = Wrappers.query();
queryWrapper.eq("name", "张三");

// 创建 LambdaQueryWrapper
LambdaQueryWrapper<User> lambdaQueryWrapper = Wrappers.lambdaQuery();
lambdaQueryWrapper.eq(User::getName, "张三");

// 创建 UpdateWrapper
UpdateWrapper<User> updateWrapper = Wrappers.update();
updateWrapper.set("name", "李四");

// 创建 LambdaUpdateWrapper
LambdaUpdateWrapper<User> lambdaUpdateWrapper = Wrappers.lambdaUpdate();
lambdaUpdateWrapper.set(User::getName, "李四");

Kotlin支持QueryWrapperUpdateWrapper,但不支持LambdaQueryWrapperLambdaUpdateWrapper。如果需要使用Lambda风格的Wrapper,可以使用KtQueryWrapperKtUpdateWrapper

因为Lambda式链式调用不支持Kotlin。

常用方法

基础比较运算

这是最常用的部分,用于构建 WHERE column = value 类型的条件。

方法 说明 SQL 示例 备注
eq 等于 (=) age = 18 最常用的方法
ne 不等于 (<>) age <> 18
gt 大于 (>) age > 18 Greater Than
ge 大于等于 (>=) age >= 18 Greater or Equal
lt 小于 (<) age < 18 Less Than
le 小于等于 (<=) age <= 18 Less or Equal
allEq 批量等于 id=1 AND name='Tom' 传入 Map,批量添加 eq 条件

分组、排序与字段选择

用于构建 GROUP BYORDER BY 和指定查询字段。

方法 说明 SQL 示例
groupBy 分组 GROUP BY id, name
having Having 子句 HAVING sum(age) > 10
orderByAsc 升序排列 ORDER BY id ASC
orderByDesc 降序排列 ORDER BY id DESC
orderBy 动态排序 ORDER BY id ASC, age DESC
select 指定查询字段 SELECT id, name

更新专用

这些方法仅在 UpdateWrapperLambdaUpdateWrapper 中使用,用于指定 SET 部分。

方法 说明 SQL 示例
set 设置字段值 SET name = '新名字'
setSql 设置 SQL 片段 SET age = age + 1
setIncrBy 字段自增 SET age = age + 1
setDecrBy 字段自减 SET age = age - 1

模糊查询与范围

用于处理字符串匹配、区间查询和空值判断。

方法 说明 SQL 示例 备注
like 模糊查询 name LIKE '%张%' 两边都有 %
notLike 非模糊查询 name NOT LIKE '%张%'
likeLeft 左模糊 name LIKE '%张' 左边有 %
likeRight 右模糊 name LIKE '张%' 右边有 % (常用于搜索框)
notLikeLeft 非左模糊 name NOT LIKE '%张'
notLikeRight 非右模糊 name NOT LIKE '张%'
between 范围查询 age BETWEEN 18 AND 20 包含边界值
notBetween 非范围查询 age NOT BETWEEN 18 AND 20
isNull 字段为空 email IS NULL
isNotNull 字段不为空 email IS NOT NULL (你列表中未列出,但也常用)
in IN 查询 id IN (1, 2, 3) 传入集合或数组
notIn NOT IN 查询 id NOT IN (1, 2, 3)

SQL 拼接

这些方法允许你直接编写 SQL 片段,MP 会自动处理参数预编译,防止 SQL 注入。

方法 说明 SQL 示例 使用场景
inSql 字段 IN (子查询) id IN (select id from user where id < 3) 子查询场景
notInSql 字段 NOT IN (子查询) id NOT IN (select id from user where id < 3)
eqSql 等于 (SQL片段) id = (select max(id) from user) 右侧是 SQL 而非固定值
gtSql/geSql 大于/大于等于 (SQL片段) age > (select avg(age) from user)
ltSql/leSql 小于/小于等于 (SQL片段) age < (select avg(age) from user)
apply 拼接原生 SQL date_format(create_time, '%Y-%m') = '2023-01' 数据库函数处理,如日期格式化
last 直接拼接到末尾 LIMIT 1 慎用,直接拼接字符串,有注入风险

逻辑组合与嵌套

用于处理复杂的 ANDOR 以及括号嵌套逻辑。

方法 说明 SQL 示例 备注
and AND 嵌套 AND (age > 10 AND name = 'a') 传入 Lambda,自动加括号
or OR 连接 age > 10 OR name = 'a' 打断默认的 AND 链式调用
nested 普通嵌套 (age > 10 OR name = 'a') 仅加括号,不加 AND/OR 前缀
func 函数式封装 - 用于抽取公共的 Wrapper 构建逻辑
exists EXISTS 查询 EXISTS (select id from ...) 判断子查询是否有结果
notExists NOT EXISTS 查询 NOT EXISTS (select id from ...)

特殊方法

lambda:

  • 这是一个转换方法。如果你在代码中已经 new 了一个 QueryWrapper,但中途想用 Lambda 写法,可以调用 .lambda() 方法将其转换为 LambdaQueryWrapper
  • 例如:queryWrapper.lambda().eq(User::getName, "Tom")
1
2
3
4
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
LambdaUpdateWrapper<User> lambdaUpdateWrapper = updateWrapper.lambda();
// 使用 Lambda 表达式构建更新条件
lambdaUpdateWrapper.set(User::getName, "李四");
  • lambda 方法返回一个 LambdaWrapper 对象,具体类型取决于调用它的 Wrapper 类型。

线程安全性

Wrapper 实例不是线程安全的,因此建议在每次使用时创建新的 Wrapper 实例。这样可以避免多线程环境下的数据竞争和潜在的错误

1
2
3
4
5
6
// 在每个方法或请求中创建新的 Wrapper 实例
public List<User> getUsersByName(String name) {
QueryWrapper<User> queryWrapper = Wrappers.query();
queryWrapper.eq("name", name);
return userMapper.selectList(queryWrapper);
}

流式查询

流式查询

流式查询(Streaming Query) 是一种逐条获取查询结果的数据库访问模式。与传统查询方式相比,它不会一次性将所有结果加载到内存中,而是通过游标(Cursor)逐条处理。

传统的 list() 查询会将所有数据一次性加载到内存中,容易导致 OutOfMemoryError (OOM)。而流式查询则是“查一条、处理一条、扔一条”,内存中始终只保持少量的数据对象,从而极大地降低内存占用。

MyBatis-Plus 从 3.5.4 版本开始支持流式查询,这是 MyBatis 的原生功能,通过 ResultHandler 接口实现结果集的流式查询。这种查询方式适用于数据跑批或处理大数据的业务场景。

BaseMapper 中,新增了多个重载方法,包括 selectList, selectByMap, selectBatchIds, selectMaps, selectObjs,这些方法可以与流式查询结合使用。

基于 Cursor (游标) 的流式查询

这是最直观的流式查询方式,类似于 JDBC 的 ResultSet。它返回一个 Cursor 对象,你可以通过遍历这个游标来逐条获取数据。

MyBatis 提供了一个叫 org.apache.ibatis.cursor.Cursor 的接口类用于流式查询,这个接口继承了 java.io.Closeablejava.lang.Iterable 接口,由此可知:

  • Cursor 是可关闭的;
  • Cursor 是可遍历的。

除此之外,Cursor 还提供了三个方法:

  • isOpen(): 用于在取数据之前判断 Cursor 对象是否是打开状态。只有当打开时 Cursor 才能取数据;
  • isConsumed(): 用于判断查询结果是否全部取完。
  • getCurrentIndex(): 返回已经获取了多少条数据

使用流式查询,则要保持对产生结果集的语句所引用的表的并发访问,因为其 查询会独占连接,所以必须尽快处理。

你需要自定义一个 Mapper 方法,返回类型必须是 Cursor<T>,并配合 @Options 注解配置游标属性。

1
2
3
4
5
6
7
public interface UserMapper extends BaseMapper<User> {
// 1. 返回类型必须是 Cursor
// 2. 使用 @Options 配置游标行为
@Select("SELECT * FROM user WHERE age > #{minAge}")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000)
Cursor<User> streamByAge(@Param("minAge") int minAge);
}
  • resultSetType = ResultSetType.FORWARD_ONLY:告诉数据库驱动,这个游标是只进的,不要缓存所有结果集。这是流式查询生效的关键。
  • fetchSize = 1000:每次从数据库网络传输中获取 1000 条数据到本地缓存。如果设为 Integer.MIN_VALUE,通常是每次只取 1 条(取决于驱动实现),设大一点可以减少网络交互次数。

在 Service 中使用, 使用 Cursor 必须在事务范围内,或者手动管理连接,否则游标会在方法结束时立即关闭,导致遍历报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class UserService {
@Autowired
private UserMapper userMapper;

// 必须加 @Transactional,保证游标在事务期间保持打开
@Transactional
public void processLargeData() {
// 使用 try-with-resources 自动关闭游标
try (Cursor<User> cursor = userMapper.streamByAge(18)) {
for (User user : cursor) {
// 逐条处理业务逻辑
System.out.println("处理用户: " + user.getName());
// doSomething(user);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}

基于 ResultHandler 的回调处理

这是 MyBatis 原生的流式处理方式,MP 3.5.4+ 在 BaseMapper 中直接封装了支持。这种方式不需要返回 Cursor,而是通过回调函数处理每一条数据。

直接调用 baseMapper.selectList 的重载方法,传入 ResultHandler

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
@Service
public class UserService {
@Autowired
private UserMapper userMapper;

public void handleWithResultHandler() {
// 构造查询条件
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.gt(User::getAge, 18);

// 调用 MP 封装的流式查询方法
userMapper.selectList(wrapper, new ResultHandler<User>() {
int count = 0;
@Override
public void handleResult(ResultContext<? extends User> resultContext) {
User user = resultContext.getResultObject();
count++;
System.out.println("当前处理第 " + count + " 条: " + user.getName());

// 业务处理逻辑...

// 如果需要提前停止,可以调用 resultContext.stop()
// if (count > 100) resultContext.stop();
}
});
}
}
  • 这样的代码更紧凑,不需要自定义 Mapper XML 或注解。但是逻辑写在回调里,不如 Cursorfor-each 循环直观。

流式查询的一些问题

事务超时问题

  • 因为流式查询需要长时间保持数据库连接来读取数据,如果你的数据处理逻辑很慢(比如处理几百万条数据耗时 1 小时),很容易触发事务超时
  • 解决:在 @Transactional 注解中设置超时时间,例如 @Transactional(timeout = 3600)

连接池耗尽

  • 流式查询会长时间占用一个数据库连接。如果并发执行多个流式查询任务,可能会把连接池(如 HikariCP)的连接占满,导致其他正常业务无法获取连接。
  • 解决:为流式查询配置独立的、较小的连接池,或者限制并发度。

必须在事务中

  • 如前所述,Cursor 依赖数据库连接保持打开。Spring 的事务管理器会在方法结束时关闭连接。所以必须@Transactional 方法内使用,或者使用 TransactionTemplate 手动控制。

不要跳过行

  • 流式查询是“流”,你不能像 List 那样随机访问(比如 list.get(500))。你必须按顺序遍历,如果你想处理第 500 条,必须先“流过”前 499 条。

MySQL 的特殊配置

  • 在 MySQL 中,要实现真正的流式查询(不缓存到内存),通常还需要在 JDBC URL 连接字符串中设置 useCursorFetch=true,或者在 Mapper 注解中明确指定 fetchSize

代码生成器

AutoGenerator 是 MyBatis-Plus 的代码生成器,通过 AutoGenerator 可以快速生成 Entity、Mapper、Mapper XML、Service、Controller 等各个模块的代码,极大的提升了开发效率。

https://baomidou.com/guides/new-code-generator/

代码生成器最少只需要mybatis-plus-boot-starter、mybatis-plus-generator、freemarker这三个依赖,如果没有添加模板引擎依赖会报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<dependencies>
<!-- 提供注解-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.12</version>
</dependency>
<!-- 代码生成器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.12</version>
</dependency>
<!-- 模板引擎-->
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<scope>compile</scope>
</dependency>
</dependencies>

创建一个 Java 类,在其中配置并执行代码生成。这是最核心的部分,主要分为全局配置、数据源配置、包配置和策略配置。常用配置的清单如下

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
public class MybatisPlusGenerator {

/**
* 常用配置清单版
*/
public static void generator2() {
// 0、使用FastAutoGenerator构造器,同时进行数据源配置
FastAutoGenerator.create(new DataSourceConfig.Builder("url","user","root"))
// 2、全局配置
.globalConfig(builder -> {
builder.outputDir((System.getProperty("user.dir")+"/src/main/java")) // 设置文件的输出目录 根目录/src/main/java
.disableOpenDir() // 禁止打开输出目录(默认 true)
.author("baomidou") // 设置作者(默认值)
.enableSwagger() // 开启 swagger 模式(默认 false),与 springdoc 不可同时使用
.commentDate("yyyy-MM-dd"); // 设置注释日期,直接使用格式也可以
})
// 3、包配置
.packageConfig(builder -> {
builder.parent("com.baomidou") // 设置父包名(默认值)
.moduleName("") // 设置模块名(默认值),就是父包名的子模块名称,适用于单体多模块项目
.pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir")+"/src/main/resources/mapper")) // 用一个map直接设置各个包的路径,主要用来修改mapper.xml路径,否则默认在java/mapper的目录下,例:Collections.singletonMap(OutputFile.mapperXml, "/path/to/xml")
.joinPackage(""); // 连接父子包名,整体目录结构为 outputDir.parent.moduleName.subPackage,适用于微服务项目
})
// 4、策略配置
.strategyConfig(builder -> {
builder
// 4.1、全局策略配置
.enableSkipView() // 开启跳过视图(默认 false)
.addInclude() // 增加表匹配(内存过滤),支持可变参数和List列表,与 addExclude 互斥,只能配置一项,支持正则匹配,如 ^t_.* 匹配所有以 t_ 开头的表名

// 4.2、Entity策略配置
.entityBuilder()
.superClass(BaseEntity.class) // 设置父类,使用公共字段时使用
.disableSerialVersionUID() // 禁用生成 serialVersionUID(默认 true)
.enableChainModel() // 开启链式模型(默认 false)
.enableLombok(new ClassAnnotationAttributes("@Data","lombok.Data")) // 开启lombok模型(默认 false),会把注解属性都加入进去,无论是否启用GlobalConfig的isKotlin(),同时get,set,toString都将不会生成,需自行控制添加
.enableTableFieldAnnotation() // 开启生成实体时生成字段注解(默认 false)
.enableActiveRecord() // 开启 ActiveRecord 模式(默认 false)
.versionColumnName("version") // 设置乐观锁字段名(数据库字段),versionColumnName 与 versionPropertyName 二选一即可
.logicDeleteColumnName("deleted") // 设置逻辑删除字段名(数据库字段),logicDeleteColumnName 与 logicDeletePropertyName 二选一即可
.addSuperEntityColumns() // 添加父类公共字段,支持可变参数和List列表
.enableFileOverride() // 开启覆盖已有文件(默认 false),针对 Entity
.disable() // 禁用 Entity 生成(默认 true)

// 4.3、Controller策略配置
.controllerBuilder()
.superClass("") // 设置父类,可以用 类全称带包名 或 父类.class
.enableRestStyle() // 开启生成 @RestController 控制器(默认 false),采用 restful 风格的api
.enableFileOverride() // 开启覆盖已有文件(默认 false),针对 Controller
.disable() // 禁用 Controller 生成(默认 true)

// 4.4、Service策略配置
.serviceBuilder()
.superServiceClass("") // 设置接口父类,可以用 类全称带包名 或 父类.class
.superServiceImplClass("") // 设置实现类父类,可以用 类全称带包名 或 父类.class
.enableFileOverride() // 开启覆盖已有文件(默认 false),针对 Service
.disable() // 禁用 Service 接口和实现类生成(默认 true)

// 4.5、Mapper策略配置
.mapperBuilder()
.superClass("") // 设置父类,可以用 类全称带包名 或 父类.class
.mapperAnnotation(Mapper.class) // 自定义添加注解,例:@Mapper注解 Mapper.class
.enableFileOverride() // 开启覆盖已有文件(默认 false),针对 Mapper
.disable(); // 禁用Mapper接口和Xml文件生成(默认 true)
})
// 6、选择模板引擎
.templateEngine(new FreemarkerTemplateEngine()) // 设置使用Freemarker引擎模板,默认的是Velocity引擎模板
// 7、执行
.execute();

}

}
java
运行

你可以向模板中注入自定义的属性,在模板文件中通过 ${cfg.你的属性名} (Freemarker) 来获取。

1
2
3
4
5
6
7
8
9
InjectionConfig injectionConfig = new InjectionConfig() {
@Override
public void initMap() {
Map<String, Object> map = new HashMap<>();
map.put("myCustomProperty", "这是一个自定义属性");
this.setMap(map);
}
};
mpg.setCfg(injectionConfig);

但是,我们很少使用配置类的代码生成器,使用 IDEA 的图形化插件,例如 MyBatisXMyBatisPlus 插件。它们提供了可视化的界面来配置数据库连接和代码生成策略,通过简单的点击就能完成代码生成,非常适合快速开发。

MyBatis 高级内容

主键生成策略

MP 的主键生成策略是通过 @TableId 注解的 type 属性或全局配置来指定

如果前面提到 @TableId 注解的 type 属性不能满足数据库原生序列的要求,需要进行额外定义,那么主键生成策略必须使用 INPUT 类型,这意味着主键值需要由用户在插入数据时提供,MP 不自动生成主键。MyBatis-Plus 内置支持多种数据库的主键生成策略,包括:

生成器类 适配数据库 核心作用
DB2KeyGenerator DB2 适配 DB2 数据库的序列生成主键
H2KeyGenerator H2 适配 H2 数据库的序列 / 自增
KingbaseKeyGenerator 人大金仓 适配国产金仓数据库的序列
OracleKeyGenerator Oracle 适配 Oracle 数据库的序列
PostgreKeyGenerator PostgreSQL 适配 PostgreSQL 的序列

下面是一个使用 @KeySequence 注解的实体类示例,就把我上面的例子改造一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;

// 指定 Oracle 序列名,clazz 表示主键类型(默认 Long,可指定 String)
@KeySequence(value = "SEQ_USER_ID", clazz = Long.class)
@Data
@TableName("user_plus")
public class User {
// 策略设为 INPUT(MP 不自动生成,由序列生成器赋值)
@TableId(value = "id", type = IdType.INPUT)
private Long id;
private String username;
private Integer age;
// 其他字段...
}

然后,需将 OracleKeyGenerator 注入 Spring 容器,MP 会自动识别并使用该生成器为 @KeySequence 注解的实体生成主键

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.baomidou.mybatisplus.extension.incrementer.OracleKeyGenerator;
import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyBatisPlusConfig {
// 注册 Oracle 主键生成器
@Bean
public IKeyGenerator keyGenerator() {
return new OracleKeyGenerator();
}
}

若项目中多个实体需要统一主键策略,可通过全局配置替代局部注解,减少重复代码

首先在application.yml 中补充全局主键配置

1
2
3
4
5
6
7
8
9
10
11
12
mybatis-plus:
global-config:
db-config:
id-type: ASSIGN_ID # 全局默认雪花算法(替代 AUTO)
key-generator: com.baomidou.mybatisplus.extension.incrementer.OracleKeyGenerator # 全局序列生成器(Oracle 场景)
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
# 其他配置(保留)
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  • 配置后,所有实体的 @TableId 若不指定 type,默认使用 ASSIGN_ID
  • 若需覆盖全局策略,只需在实体类的 @TableId 中指定 type,如 IdType.AUTO

若内置生成器无法适配你的业务,如自定义雪花算法、业务规则生成主键,可实现 IKeyGenerator 接口来扩展

  • 实现 IKeyGenerator 接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
    import org.springframework.stereotype.Component;

    import java.util.UUID;

    // 自定义主键生成器(生成 "USER_" + UUID 的字符串主键)
    @Component
    public class CustomKeyGenerator implements IKeyGenerator {
    @Override
    public String executeSql(String incrementerName) {
    // incrementerName 对应 @KeySequence 的 value 属性
    return "USER_" + UUID.randomUUID().toString().replace("-", "");
    }
    }
  • 注册生成器

    1
    2
    3
    4
    5
    6
    7
    @Configuration
    public class MyBatisPlusConfig {
    @Bean
    public IKeyGenerator customKeyGenerator() {
    return new CustomKeyGenerator();
    }
    }
  • 实体类使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    @KeySequence(value = "CUSTOM_SEQ", clazz = String.class) // 自定义序列名
    @Data
    @TableName("user_plus")
    public class User {
    @TableId(type = IdType.INPUT) // 必须用 INPUT 策略
    private String id; // 主键类型改为 String
    private String username;
    private Integer age;
    }

自定义ID生成器

首先看 MyBatis-Plus 自带主键生成策略对比,这是 IdentifierGenerator 接口中的两个方法

方法 主键生成策略 主键类型 说明
nextId ASSIGN_ID Long,Integer,String 支持自动转换为String类型,但数值类型不支持自动转换,需精准匹配,例如返回Long,实体主键就不支持定义为Integer
nextUUID ASSIGN_UUID String 默认不含中划线的UUID生成

MyBatis-Plus 提供了多种方式来实现自定义ID生成器,以下是一些示例,还是基于上面改造的

声明为 Bean 供 Spring 扫描注入,也就是说,实现 IdentifierGenerator 接口

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class CustomIdGenerator implements IdentifierGenerator {
@Override
public Long nextId(Object entity) {
// 使用实体类名作为业务键,或者提取参数生成业务键
String bizKey = entity.getClass().getName();
// 根据业务键调用分布式ID生成服务
long id = ...; // 调用分布式ID生成逻辑
// 返回生成的ID值
return id;
}
}

然后用配置类注册

1
2
3
4
@Bean
public IdentifierGenerator idGenerator() {
return new CustomIdGenerator();
}

然后怎么让 MyBatis-Plus 使用这个自定义生成器?

1
2
3
4
5
6
7
8
@TableName("user")
public class User {

@TableId(type = IdType.ASSIGN_ID)
private Long id;

// ...
}

对于nextId,使用ASSIGN_ID 让 MP 调用 IdentifierGenerator,自定义的 CustomIdGenerator自动生效

实现 IKeyGenerator 接口和IdentifierGenerator接口来扩展看起来类似,但是二者有根本上的区别

接口 全称 作用 / 定位 适用场景
IKeyGenerator 数据库键生成器 靠数据库生成主键(序列、自增) Oracle、DB2、PostgreSQL依赖数据库序列的场景
IdentifierGenerator 标识符生成器 靠 Java 代码生成主键(雪花算法、UUID) MySQL、分布式系统不依赖数据库,纯代码生成
  • 对于IKeyGenerator生成主键的工作交给数据库,MP 只是去数据库拿值。
  • 对于IdentifierGenerator,在 Java 代码里直接生成 ID,MP 默认的 雪花算法、UUID 都是它实现的。

逻辑删除支持

逻辑删除是一种优雅的数据管理策略,它通过在数据库中标记记录为“已删除”而非物理删除,来保留数据的历史痕迹,同时确保查询结果的整洁性。MyBatis-Plus 提供了便捷的逻辑删除支持,使得这一策略的实施变得简单高效。

MyBatis-Plus 的逻辑删除功能会在执行数据库操作时自动处理逻辑删除字段。以下是它的工作方式:

  • 插入:逻辑删除字段的值不受限制。
  • 查找:自动添加条件,过滤掉标记为已删除的记录。
  • 更新:防止更新已删除的记录。
  • 删除:将删除操作转换为更新操作,标记记录为已删除。

例如:

  • 删除update user set deleted=1 where id = 1 and deleted=0
  • 查找select id,name,deleted from user where deleted=0

对于字段类型支持,虽然不仅仅是 0 和 1,但是一般情况下,还是用0和1

如果是需要全局配置,在 application.yml 中统一配置,适用于全项目统一的逻辑删除规范。

1
2
3
4
5
6
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted # 全局逻辑删除字段名
logic-delete-value: 1 # 逻辑已删除值 (默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值 (默认为 0)
  • 配置一次,所有实体类只要包含名为 deleted 的字段,都会自动生效,无需重复加注解。

局部配置就是在实体类的字段上使用 @TableLogic 注解

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.baomidou.mybatisplus.annotation.TableLogic;

public class User {
// ...其他字段

// 单独配置:0表示未删除,1表示已删除
@TableLogic
private Integer deleted;

// 自定义值示例:使用状态字段
// @TableLogic(value = "0", delval = "2")
// private Integer status;
}

配置好后,MP 会在底层自动拦截并修改 SQL 语句,这对业务代码是透明的。

(确保使用的是 MP 提供的 deleteById 方法。如果是自定义 SQL,MP 的逻辑删除拦截器可能不会生效,需要手动处理。)

操作类型 你的代码调用 MP 生成的实际 SQL 说明
删除 userMapper.deleteById(1L) UPDATE user SET deleted=1 WHERE id=1 AND deleted=0 DELETE 被转换为 UPDATE,并追加 deleted=0 条件防止重复删除。
查询 userMapper.selectList(...) SELECT * FROM user WHERE ... AND deleted=0 所有查询自动追加 AND deleted=0,确保查不到已删除数据。
更新 userMapper.updateById(user) UPDATE user SET ... WHERE id=1 AND deleted=0 更新时也保护性地加上条件,防止更新已删除数据。

逻辑删除字段通常在插入时应该是“未删除”状态。所以说,一般情况下,在建表时设置 deleted 字段默认值为 0是很好的实践。当然你也可以通过自动填充类似审计的功能,使用 @TableField(fill = FieldFill.INSERT) 配合 MetaObjectHandler 自动填充 0

但是逻辑删除 + 唯一索引冲突的情况怎么办

假设 username 字段有唯一索引。用户 A 删除了账号 zhangsan(逻辑删除,deleted=1)。此时如果你想重新注册一个 zhangsan,数据库会报错“唯一键冲突”,因为数据库里还存着那条 deleted=1 的记录。

这时候,使用 0 和 1 就显得很局限了,我们试着将将逻辑删除字段设为 datetime 类型。未删除值为 'null',已删除值为 'now()'。然后将唯一索引改为联合唯一索引 UNIQUE KEY idx_username_del (username, deleted)

  • 正常数据:deletedNULLusername 必须唯一。
  • 删除数据:deleted 变为具体时间(如 2023-10-01 12:00:00)。
  • 新注册:因为 deletedNULL,它与库中已删除的(deleted 不为 NULL)记录不冲突,完美解决!

批量操作

批量操作是一种高效处理大量数据的技术,它允许开发者一次性执行多个数据库操作,从而减少与数据库的交互次数,提高数据处理的效率和性能。在MyBatis-Plus中,批量操作主要用于以下几个方面:

  • 数据插入(Insert):批量插入是批量操作中最常见的应用场景之一。通过一次性插入多条记录,可以显著减少SQL语句的执行次数,加快数据写入速度。这在数据迁移、初始化数据等场景中尤为有用。

    使用 saveBatch 时,如果数据库是 MySQL 且开启了 rewriteBatchedStatements=true可能无法获取自动生成的主键 ID

  • 数据更新(Update):批量更新允许同时修改多条记录的特定字段,适用于需要对大量数据进行统一变更的情况,如批量修改用户状态、更新产品价格等。

  • 数据删除(Delete):批量删除操作可以快速移除数据库中的多条记录,常用于数据清理、用户注销等场景。

那么,批量操作很明显和流式查询是不太一样的,不要混为一谈

  • 流式查询是为了解决读取大量数据时的内存溢出问题(读)。
  • 批量操作是为了解决写入/修改大量数据时的性能瓶颈(写)。

MP 的批量操作底层依赖的是 JDBC 的 addBatch() / executeBatch() 机制

开启事务时:

  • MP 会获取 SqlSession
  • 将 SQL 加入 Batch(addBatch),但不立即发送给数据库。
  • 当达到批次大小(默认 1000 条)或循环结束时,统一执行 executeBatch()
  • 最后提交事务。

所以说不管是流失查询还是批量操作,都建议配合 @Transactional 注解使用!

MP 的 IService 接口提供了两个最常用的批量方法:saveBatchupdateBatchById

  • 批量插入:saveBatch

    用于一次性插入大量数据。

    1
    2
    3
    4
    5
    6
    7
    8
    // 准备 1000 个用户
    List<User> userList = new ArrayList<>();
    for (int i = 0; i < 1000; i++) {
    userList.add(new User("User" + i, i));
    }

    // 执行批量插入
    boolean success = userService.saveBatch(userList);
  • 批量更新:updateBatchById

    用于根据 ID 批量更新数据。

    1
    2
    3
    4
    5
    6
    7
    // 准备更新数据
    List<User> userList = new ArrayList<>();
    userList.add(new User(1L, "NewName1", 20));
    userList.add(new User(2L, "NewName2", 21));

    // 执行批量更新
    boolean success = userService.updateBatchById(userList);
  • 批量删除:MP 没有专门的 removeBatchByIds 方法在 Service 层直接暴露。用Mapper层中的deleteBatchIds

    1
    2
    // 批量删除 ID 为 1, 2, 3 的用户
    userMapper.deleteBatchIds(Arrays.asList(1, 2, 3));

默认情况下,即使使用了 saveBatch,MySQL 驱动发送的 SQL 可能是这样的:

1
2
3
4
INSERT INTO user (name, age) VALUES (?, ?);
INSERT INTO user (name, age) VALUES (?, ?);
INSERT INTO user (name, age) VALUES (?, ?);
...

虽然是一次性发送,但数据库还是把它看作多条独立的插入语句,解析开销依然存在。

在 JDBC 连接 URL 中添加 rewriteBatchedStatements=true 参数:

1
jdbc:mysql://localhost:3306/mydb?rewriteBatchedStatements=true

开启后,MySQL 驱动会将多条 INSERT 语句合并为一条真正的多值插入:

1
INSERT INTO user (name, age) VALUES (?, ?), (?, ?), (?, ?);

字段脱敏

字段脱敏字段加密不同,字段脱敏通常是指数据在数据库中是明文,但在查询出来返回给前端时,自动将中间几位替换为星号

使用 mybatis-mate,然后在实体类的敏感字段上添加 @FieldSensitive 注解。MP 内置了 9 种常用的脱敏策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.baomidou.mybatisplus.extension.handlers.FieldSensitive;
import com.baomidou.mybatisplus.extension.handlers.SensitiveType;

public class User {
private Long id;

// 使用内置策略:手机号脱敏
@FieldSensitive(type = SensitiveType.mobile)
private String mobile;

// 使用内置策略:身份证号脱敏
@FieldSensitive(type = SensitiveType.idCard)
private String idCard;

// 使用自定义策略
@FieldSensitive(type = "myCustomStrategy")
private String username;
}

如果内置策略不满足需求,你可以定义自己的策略并注册到 Spring 容器中:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class SensitiveStrategyConfig {
@Bean
public ISensitiveStrategy sensitiveStrategy() {
// 注册一个名为 "myCustomStrategy" 的策略
return new SensitiveStrategy().addStrategy("myCustomStrategy", value -> {
// 自定义逻辑:保留前2位,后面全部变星号
if (value == null) return null;
return value.substring(0, 2) + "****";
});
}
}

这样,查询后自动处理,返回给前端就是脱敏后的数据。如果在编辑场景需要明文,可以调用 RequestDataTransfer.skipSensitive() 来临时跳过。

使用 Jackson 注解指定序列器这种方式比较常规和通用,我就不再强调了

多数据源支持

随着项目规模的扩大,单一数据源已无法满足复杂业务需求,多数据源(动态数据源)应运而生。本文将介绍两种 MyBatis-Plus 的多数据源扩展插件:开源生态的 dynamic-datasource 和 企业级生态的 mybatis-mate

这里只说dynamic-datasource,使用它非常简单

dynamic-datasource 是一个开源的 Spring Boot 多数据源启动器,提供了丰富的功能,包括数据源分组、敏感信息加密、独立初始化表结构等

1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>${version}</version>
</dependency>

配置数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
datasource:
dynamic:
primary: master
strict: false
datasource:
master:
url: jdbc:mysql://xx.xx.xx.xx:3306/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave_1:
url: jdbc:mysql://xx.xx.xx.xx:3307/dynamic
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
slave_2:
url: ENC(xxxxx)
username: ENC(xxxxx)
password: ENC(xxxxx)
driver-class-name: com.mysql.jdbc.Driver

使用 @DS 切换数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
@DS("slave")
public class UserServiceImpl implements UserService {

@Autowired
private JdbcTemplate jdbcTemplate;

@Override
@DS("slave_1")
public List selectByCondition() {
return jdbcTemplate.queryForList("select * from user where age >10");
}
}