MyBatis Plus 高级用法实战:解锁分页、乐观锁与自动填充黑科技
引言
MyBatis Plus 作为 MyBatis 的增强工具,凭借其强大的 CRUD 接口和开箱即用的插件生态,已成为 Java 开发中不可或缺的 ORM 框架。在掌握了基础的 BaseMapper 和条件构造器后,你是否遇到过这些痛点?分页查询需要手动拼装 SQL、并发更新出现数据覆盖、字段自动填充逻辑散落各处…… 本文将以一个完整的 Spring Boot 项目为蓝本,深入讲解 MyBatis Plus 的高级特性,包括分页插件、乐观锁、逻辑删除、自动填充、自定义 SQL 注入等,所有代码均可直接运行,助你从根本上提升开发效率。
1. 核心高级特性概览
在深入编码之前,我们先快速盘点 MyBatis Plus 提供的关键高级能力:
- 分页插件:一行配置即可实现物理分页,无需在 SQL 中手动拼接
LIMIT,支持多数据库方言。 - 乐观锁:通过
@Version注解,在更新时自动拼接版本号校验,防止并发更新覆盖。 - 逻辑删除:配置实体字段后,删除操作自动转为
UPDATE,查询时自动添加deleted=0条件。 - 自动填充:基于
MetaObjectHandler统一处理create_time、update_time等字段,告别重复set代码。 - 自定义 SQL 注入器:扩展
SqlInjector或使用AbstractMethod,在BaseMapper中增加全局通用方法。 - 多租户 & 动态表名:通过插件拦截 SQL,动态替换表名或追加租户过滤条件。
- 类型处理器:优雅地将 JSON 字段映射为 Java 对象,避免冗余的序列化代码。
本文将重点演示前五项特性的实战组合,覆盖绝大多数业务场景。
2. 项目环境准备
创建一个 Spring Boot 2.7+ 项目,引入以下依赖(pom.xml 核心片段):
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
数据库建表语句(包含逻辑删除、版本号、时间字段):
CREATE TABLE `t_user` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL,
`age` int DEFAULT NULL,
`email` varchar(100) DEFAULT NULL,
`version` int DEFAULT '1' COMMENT '乐观锁版本号',
`deleted` tinyint DEFAULT '0' COMMENT '逻辑删除(0未删除 1已删除)',
`create_time` datetime DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
);
3. 全局配置与插件注册
MyBatis Plus 的诸多高级特性均依赖于插件或全局配置。创建一个配置类,统一注册分页插件、乐观锁插件,并指定逻辑删除与自动填充处理器。
@Configuration
@MapperScan("com.example.mpadvanced.mapper")
public class MybatisPlusConfig {
/**
* 分页插件(支持多种数据库,此处以MySQL为例)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 分页插件
PaginationInnerInterceptor paginationInnerInterceptor =
new PaginationInnerInterceptor(DbType.MYSQL);
// 设置最大单页限制 500 条
paginationInnerInterceptor.setMaxLimit(500L);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
/**
* 逻辑删除字段与值配置(方式一:配置文件,此处演示注解方式)
* 将在实体类中使用 @TableLogic 进行声明,此处不再重复配置
*/
/**
* 自动填充字段处理器
*/
@Bean
public MetaObjectHandler metaObjectHandler() {
return new MyMetaObjectHandler();
}
}
4. 实体类设计(整合所有特性)
使用 Lombok 简化代码,实体类通过注解声明逻辑删除、乐观锁与自动填充字段。
@Data
@TableName("t_user")
public class User implements Serializable {
@TableId(type = IdType.ASSIGN_ID) // 雪花算法
private Long id;
private String name;
private Integer age;
private String email;
@Version // 乐观锁版本号
private Integer version;
@TableLogic(value = "0", delval = "1") // 逻辑删除
private Integer deleted;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
5. 自动填充处理器实现
统一填充 create_time 和 update_time,避免在业务代码中手动赋值。
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
@Override
public void updateFill(MetaObject metaObject) {
this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
}
}
6. Mapper 接口与自定义全局方法
定义 UserMapper,并借此演示自定义 SQL 注入:我们可以在基类中增加一个 alwaysUpdateSomeColumnById 方法,类似 updateById 但可指定仅更新非空字段,或添加其他通用逻辑。这里展示扩展 BaseMapper 注入 deleteAll 方法(谨慎使用)。
public interface UserMapper extends BaseMapper<User> {
// 普通自定义SQL(混合MP条件构造器)
@Select("SELECT * FROM t_user WHERE age > #{age} AND deleted = 0")
List<User> selectByMinAge(@Param("age") int age);
// 与条件构造器结合的高级查询可以在Service层完成
}
自定义通用方法(SQL注入器示例):
创建 DeleteAll 方法实现,并将其注入到 BaseMapper 中。
// 自定义方法,注意继承 AbstractMethod
public class DeleteAll extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql = "DELETE FROM " + tableInfo.getTableName();
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
return this.addDeleteMappedStatement(mapperClass, "deleteAll", sqlSource);
}
}
注册该注入器:
@Component
public class MySqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo);
methodList.add(new DeleteAll());
return methodList;
}
}
然后在配置类中替换默认注入器(可选,此处略),或直接继承 BaseMapper 扩展,集成起来稍有侵入性。为保持示例简洁,本文重点放在更高频的特性上。
7. Service 层实战:组合高级特性
编写 UserService,演示分页查询、乐观锁更新、逻辑删除与自动填充的联动效果。
@Service
public class UserService extends ServiceImpl<UserMapper, User> {
// 分页查询(使用MP的分页对象)
public IPage<User> selectPageByAge(int age, int current, int size) {
Page<User> page = new Page<>(current, size);
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.gt(User::getAge, age).orderByDesc(User::getCreateTime);
return this.page(page, wrapper);
}
// 利用乐观锁更新用户邮箱
public boolean updateEmailWithOptimisticLock(Long userId, String newEmail) {
// 先查询,获得版本号
User user = this.getById(userId);
if (user == null) {
return false;
}
user.setEmail(newEmail);
// 执行更新,自动在WHERE中拼接 version = ?
return this.updateById(user);
}
// 逻辑删除
public boolean softDeleteById(Long id) {
// 实际执行 UPDATE t_user SET deleted = 1 WHERE id = ? AND deleted = 0
return this.removeById(id);
}
}
8. 测试验证
编写单元测试或直接启动项目调用接口,观察控制台打印的 SQL 日志。
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService;
@Test
void testInsert() {
User user = new User();
user.setName("张三");
user.setAge(25);
user.setEmail("zhangsan@example.com");
boolean saved = userService.save(user);
Assertions.assertTrue(saved);
// 观察控制台:create_time和update_time自动填充,version默认为1,deleted为0
}
@Test
void testOptimisticLock() {
// 模拟并发:两个查询拿到的版本号都是1,第一个更新成功,第二个失败
User u1 = userService.getById(1L);
User u2 = userService.getById(1L);
u1.setEmail("new1@x.com");
u2.setEmail("new2@x.com");
boolean r1 = userService.updateById(u1); // 成功,版本号变2
boolean r2 = userService.updateById(u2); // 失败,因为版本号条件不匹配
Assertions.assertTrue(r1);
Assertions.assertFalse(r2);
}
@Test
void testLogicDelete() {
boolean removed = userService.softDeleteById(1L);
Assertions.assertTrue(removed);
// 执行查询:User u = userService.getById(1L); 将返回null,因为自动拼接deleted=0
User user = userService.getById(1L);
Assertions.assertNull(user);
}
@Test
void testPage() {
IPage<User> page = userService.selectPageByAge(20, 1, 5);
System.out.println("总条数:" + page.getTotal());
page.getRecords().forEach(System.out::println);
}
}
在所有测试中,我们可以看到:
- 插入时 create_time 和 update_time 自动填充当前时间。
- 乐观锁更新生成的 SQL 为 UPDATE t_user SET email=?, version=?, update_time=? WHERE id=? AND version=? AND deleted=0,若版本号不匹配则更新 0 行。
- 逻辑删除 SQL 为 UPDATE t_user SET deleted=1 WHERE id=? AND deleted=0;后续查询自动加上 deleted=0 条件。
- 分页查询执行 SELECT count(*) FROM t_user WHERE age > ? AND deleted=0 作为 total 计数,再执行分页 SQL 包含 LIMIT ?。
9. 常见问题与注意事项
9.1 乐观锁需要 version 字段
必须为整数类型,且数据库有默认值(如 1)。updateById 方法会自动使用当前对象中的 version 值进行“比较并更新”,因此 务必先查询再更新,否则会丢失版本号。Spring 事务嵌套中需注意对象版本一致性。
9.2 逻辑删除全局配置
除了使用 @TableLogic 注解,也可以在 application.yml 中统一配置:
mybatis-plus:
global-config:
db-config:
logic-delete-field: deleted
logic-delete-value: 1
logic-not-delete-value: 0
注意:全局配置下,实体类无需 @TableLogic 注解,字段名匹配即可。建议项目采用一种方式即可。
9.3 分页插件必须注册
分页插件不会自动生效,务必在配置类中注册 PaginationInnerInterceptor,否则分页参数会被忽略(返回全量数据)。
9.4 自动填充与数据库默认值冲突
当数据库也设置了 DEFAULT CURRENT_TIMESTAMP 时,二者可能重复。建议选择一种:要么仅依赖应用层自动填充,数据库不设默认值;要么关闭应用层填充,依赖数据库 DEFAULT。混合使用可能导致时间不一致。
9.5 自定义 SQL 注入器的风险
注入 deleteAll 等破坏性通用方法需谨慎评估。推荐使用 InsertBatchSomeColumn 等有明确业务语义的方法,并仅对特定 Mapper 开放。
10. 总结
MyBatis Plus 的高级特性能够极大简化日常开发中的样板代码,使开发者更加专注于业务逻辑。本文通过一个完整的用户增删改查示例,展示了分页、乐观锁、逻辑删除、自动填充与全局方法注入的整合用法。在实际项目中,这些特性可以灵活组合,再配合多租户、动态表名等插件,能够应对绝大多数企业级场景。
常见的坑点主要集中在插件未注册、乐观锁未先查询、逻辑删除全局与局部配置冲突等方面,只要掌握其底层原理并遵循最佳实践,即可避开。希望本文能成为你进阶 MyBatis Plus 的加速器,让你的代码更加干净、健壮。如果你在实践中有更多高级需求,不妨翻阅官方文档,那里还有更丰富的拦截器与扩展点等你发掘。
参考资源:
- MyBatis Plus 官方文档
- Spring Boot 集成 MyBatis Plus 最佳实践
完整项目源码 可关注本文所属专栏,后续将提供 GitHub 链接。
更多推荐


所有评论(0)