MyBatis Plus 高级用法实战:解锁分页、乐观锁与自动填充黑科技

发布时间:2026/7/10 16:00:19
MyBatis Plus 高级用法实战:解锁分页、乐观锁与自动填充黑科技 引言MyBatis Plus 作为 MyBatis 的增强工具凭借其强大的 CRUD 接口和开箱即用的插件生态已成为 Java 开发中不可或缺的 ORM 框架。在掌握了基础的BaseMapper和条件构造器后你是否遇到过这些痛点分页查询需要手动拼装 SQL、并发更新出现数据覆盖、字段自动填充逻辑散落各处…… 本文将以一个完整的 Spring Boot 项目为蓝本深入讲解 MyBatis Plus 的高级特性包括分页插件、乐观锁、逻辑删除、自动填充、自定义 SQL 注入等所有代码均可直接运行助你从根本上提升开发效率。1. 核心高级特性概览在深入编码之前我们先快速盘点 MyBatis Plus 提供的关键高级能力分页插件一行配置即可实现物理分页无需在 SQL 中手动拼接LIMIT支持多数据库方言。乐观锁通过Version注解在更新时自动拼接版本号校验防止并发更新覆盖。逻辑删除配置实体字段后删除操作自动转为UPDATE查询时自动添加deleted0条件。自动填充基于MetaObjectHandler统一处理create_time、update_time等字段告别重复set代码。自定义 SQL 注入器扩展SqlInjector或使用AbstractMethod在BaseMapper中增加全局通用方法。多租户 动态表名通过插件拦截 SQL动态替换表名或追加租户过滤条件。类型处理器优雅地将 JSON 字段映射为 Java 对象避免冗余的序列化代码。本文将重点演示前五项特性的实战组合覆盖绝大多数业务场景。2. 项目环境准备创建一个 Spring Boot 2.7 项目引入以下依赖pom.xml核心片段dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/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 BaseMapperUser { // 普通自定义SQL混合MP条件构造器 Select(SELECT * FROM t_user WHERE age #{age} AND deleted 0) ListUser 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 ListAbstractMethod getMethodList(Class? mapperClass, TableInfo tableInfo) { ListAbstractMethod methodList super.getMethodList(mapperClass, tableInfo); methodList.add(new DeleteAll()); return methodList; } }然后在配置类中替换默认注入器可选此处略或直接继承BaseMapper扩展集成起来稍有侵入性。为保持示例简洁本文重点放在更高频的特性上。7. Service 层实战组合高级特性编写UserService演示分页查询、乐观锁更新、逻辑删除与自动填充的联动效果。Service public class UserService extends ServiceImplUserMapper, User { // 分页查询使用MP的分页对象 public IPageUser selectPageByAge(int age, int current, int size) { PageUser page new Page(current, size); LambdaQueryWrapperUser 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(zhangsanexample.com); boolean saved userService.save(user); Assertions.assertTrue(saved); // 观察控制台create_time和update_time自动填充version默认为1deleted为0 } Test void testOptimisticLock() { // 模拟并发两个查询拿到的版本号都是1第一个更新成功第二个失败 User u1 userService.getById(1L); User u2 userService.getById(1L); u1.setEmail(new1x.com); u2.setEmail(new2x.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因为自动拼接deleted0 User user userService.getById(1L); Assertions.assertNull(user); } Test void testPage() { IPageUser 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 deleted0若版本号不匹配则更新 0 行。- 逻辑删除 SQL 为UPDATE t_user SET deleted1 WHERE id? AND deleted0后续查询自动加上deleted0条件。- 分页查询执行SELECT count(*) FROM t_user WHERE age ? AND deleted0作为 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 链接。