MyBatis-Plus 从零配置到进阶实战:分页、逻辑删除、自动填充与避坑指南

发布时间:2026/10/14 6:16:18

MyBatis-Plus 从零配置到进阶实战:分页、逻辑删除、自动填充与避坑指南 最近在整理项目基础代码的时候发现不少团队已经把 MyBatis-Plus 作为默认的持久层框架。说实话早几年我还在手写 XML 里的 resultMap、动态 SQL 的时候根本没想过 CRUD 能省到这种程度。这篇笔记不是官方文档的复述而是我从零配置 MyBatis-Plus 到跑通分页、逻辑删除、自动填充整个过程留下的记录和踩坑整理。适合刚接触 MP、准备在 Spring Boot 项目里引入它的同学参考也适合那些已经在用但只停留在“能用”阶段、想搞清楚配置细节的开发者。1. 为什么选择 MyBatis-Plus核心价值与适用场景1.1 从传统 MyBatis 的痛点说起先聊聊我最早用 MyBatis 的感受。单表 CRUD 在业务里其实占了一大半但传统 MyBatis 要求每个方法都要写 SQL哪怕是一个简单的selectById也得在 Mapper 接口里声明再去 XML 里写对应的 SELECT 语句。更麻烦的是联表查询时的 resultMap 映射字段一多XML 文件就变得又臭又长review 代码的时候大部分时间都在看重复的映射配置。那时候我就在想如果能把单表 CRUD 的模板方法直接生成好、把条件查询抽象成链式 API我只需要关心那些真正复杂的连表查询和业务 SQL开发效率会提升很多。这也是 MyBatis-Plus 最初吸引我的点。它不是一个全新的 ORM而是在 MyBatis 基础上做增强保留了 MyBatis 原有的灵活性和 SQL 控制力同时把单表操作、分页、逻辑删除这些高频场景从“手写”变成了“配置”。1.2 MyBatis-Plus 的核心特性速览我把它和 JPA、通用 Mapper 做过对比。JPA 在复杂查询时容易陷入 JPQL 和原生 SQL 之间的拧巴通用 Mapper 虽然早期解决了单表 CRUD但在条件构造的灵活性和更新策略上不够顺手。MyBatis-Plus 的核心特性可以归纳成这几块通用 CRUD继承BaseMapperT之后insert、deleteById、updateById、selectById、selectList这些方法开箱即用不需要写一行 SQL。条件构造器QueryWrapper、UpdateWrapper、LambdaQueryWrapper这套封装能构造出链式的动态条件不用再去 XML 里拼if标签。分页插件内置物理分页基于数据库方言自动生成 LIMIT 语句避免内存分页假象。逻辑删除通过全局配置和字段注解把删除操作自动转成update实现数据不物理删除。自动填充用于统一处理创建时间、更新时间、操作人等字段自己不用每次手动 set。乐观锁插件配合版本号字段解决并发更新覆盖问题。这些特性基本都是“约定优于配置”默认行为很聪明但一旦需要踩刹车它也给了足够多的自定义接口。1.3 什么项目适合引入 MyBatis-Plus不是所有项目都适合。如果项目里全是复杂的多表 join 和存储过程那 MyBatis-Plus 的帮助有限反倒因为多了自动生成的规则和插件增加了理解成本。但如果你做的是常规业务系统比如后台管理、会员中心、订单系统单表 CRUD 占了七八成那 MP 就能直接帮你把大量样板代码省掉。剩下那两三成复杂 SQL仍然可以用 MyBatis 原生的Select注解或者 XML 去写MP 完全不会干涉。我个人的建议是新项目如果用 Spring Boot MySQL 这套组合可以直接引入 MP只要注意不要让团队过度依赖它的“一把梭”能力复杂查询该写 SQL 还是要写 SQL。老项目想迁移也不是不行但要看现有 Mapper 里有没有大量依赖 XML 的复杂映射如果有改造成本会高于收益建议先拿一个独立模块做试点。2. 环境准备与基础配置2.1 依赖引入与版本选择这里以 Spring Boot 2.x 项目为例。我目前用的是 MyBatis-Plus 3.5.x 和 Spring Boot 2.7.x版本兼容性比较稳定。如果你用的 Spring Boot 3.x需要注意 MP 3.5.3 以上才有对应的 starter且包路径略有变化。引入依赖时直接使用官方 starter 即可dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency如果是多模块工程建议在父 pom 的dependencyManagement里统一管理版本避免子模块各自指定导致版本冲突。还要注意一点如果你之前单独引入了mybatis和mybatis-spring再引入 MP starter 可能会产生依赖冲突。MP starter 本身包含了 MyBatis 的依赖所以通常只需要保留 MP 的 starter不需要额外重复引用。2.2 Spring Boot 配置文件详解MP 提供了大量配置项但真正项目里常用的就那几个。我在application.yml里的典型配置如下spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/demo?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml type-aliases-package: com.demo.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: banner: false db-config: id-type: auto table-prefix: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0mapper-locations用来指定 XML 文件位置如果没有 XML 可以不加。map-underscore-to-camel-case必须设为 true这样数据库字段user_name才能自动映射到实体属性userName不用每个字段都写TableField。log-impl设为 StdOutImpl 是方便开发时在控制台看 SQL生产环境可以去掉或换成别的日志实现。id-type: auto表示主键使用数据库自增。如果你的表主键是自定义分配比如雪花 ID那可以改成assign_id。MP 默认的策略是ASSIGN_ID也就是雪花 ID这个细节非常容易踩坑很多人把表设置成自增主键结果插入后 entity 的主键变成了一长串雪花 ID就是因为没有显式配置。2.3 启动类扫描与 Mapper 接口定义Spring Boot 启动类上需要加MapperScan否则 Mapper 接口不会注册成 Bean。当然也可以在每个 Mapper 接口上加Mapper但项目里 Mapper 一多逐个加注解太啰嗦我一般统一在启动类上扫描SpringBootApplication MapperScan(com.demo.mapper) public class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); } }Mapper 接口本身没什么特殊要求直接继承BaseMapper就行泛型指向对应的实体类public interface UserMapper extends BaseMapperUser { // 除了继承的方法之外有需要可以自己声明方法 }实体类则建议和表名、字段名严格对应。如果表名和类名不一致用TableName(t_user)指定如果某个字段不想映射用TableField(exist false)标记。这里我个人的习惯是实体用 Lombok 的Data再加TableName明示表名避免团队里有人改类名导致映射错误。3. 核心功能我这样用CRUD、条件构造器与分页3.1 BaseMapper 的通用 CRUDBaseMapper 提供的方法足够应付单表操作。我用的最多的是这几个insert(entity)插入一条记录回填主键。selectById(id)根据主键查一条记录返回 null 表示不存在。selectBatchIds(ids)根据主键集合批量查询。updateById(entity)根据主键更新非 null 字段。selectList(wrapper)查询列表wrapper 可以传 null。deleteById(id)逻辑删除开启后底层会变成 update 语句。delete(wrapper)按条件删除同样受逻辑删除配置影响。有一个特别容易忽略的点updateById默认只更新非 null 字段。如果你想把某个字段更新成 null直接传 entity 是不生效的需要改用UpdateWrapper并且用.set(field, null)显式设置。这是很多测试新人第一次疑惑的地方“我明明把字段置空了为什么数据库没变”原因就在这里。3.2 条件构造器 QueryWrapper / LambdaQueryWrapper条件构造器是 MP 使用频率最高的能力。我最早用QueryWrapper后来改成LambdaQueryWrapper原因是 Lambda 方式能把字段名写错的问题从运行时提前到编译期。比如要查所有状态为启用的用户按创建时间倒序ListUser users userMapper.selectList( new LambdaQueryWrapperUser() .eq(User::getStatus, 1) .orderByDesc(User::getCreateTime) );如果还要支持多个条件条件构造器里可以继续链式追加LambdaQueryWrapperUser wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(name), User::getName, name) .ge(beginTime ! null, User::getCreateTime, beginTime) .le(endTime ! null, User::getCreateTime, endTime) .eq(User::getStatus, 1) .orderByDesc(User::getCreateTime); ListUser list userMapper.selectList(wrapper);这里用like(StringUtils.hasText(name), ...)这种重载方法是 MP 非常贴心的设计第一个 boolean 条件为 true 时才拼接后面的查询条件相当于把原来 XML 里的if判断搬到 Java 里代码清爽很多。不过要注意QueryWrapper如果使用了实体类属性名对应的数据库字段字符串写错不会立刻报错而是 SQL 执行时才出错所以我建议团队统一使用LambdaQueryWrapper。3.3 分页插件配置与使用分页插件需要先配置一个拦截器。我在配置类里写的Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(100L); interceptor.addInnerInterceptor(pagination); return interceptor; } }分页插件是比较早的 com.baomidou 提供的分页器我之前用过一个叫PaginationInterceptor在 MP 3.5.x 里已经改成PaginationInnerInterceptor要特别注意版本差异。setMaxLimit是可选配置作用是对单次分页查询的最大条数做限制防止有人一次把整张表读出来。使用分页的方式是调用selectPagePageUser page new Page(current, size); // 可以设置排序比如 page.addOrder(OrderItem.desc(create_time)) PageUser result userMapper.selectPage(page, wrapper); ListUser records result.getRecords(); long total result.getTotal();千万不要在 service 层循环调用分页查询正确做法是先查出总数再查数据。MP 的selectPage会自动执行一个 count 查询和一个分页查询result.getTotal()已经帮你算好总数了。分页插件对复杂 SQL 的 count 优化不够好如果 SQL 里有多个 left join生成的 count 可能偏慢。遇到这种情况可以单独写一个 count SQL或者在分页前用optimizeJoin之类的插件做优化。虽然复杂场景不多但真遇到时排查方向要正确。4. 进阶功能配置与避坑逻辑删除、自动填充、乐观锁4.1 逻辑删除的全局配置业务系统里“删除”往往是假删数据不能真的从库里消失。MP 的逻辑删除做得很简单声明一个字段代表逻辑删除标识然后在配置里定义删除值和非删除值。实体类示例Data TableName(user) public class User { TableId(type IdType.AUTO) private Long id; private String name; TableLogic private Integer deleted; }配置里的logic-delete-field: deleted是全局指定逻辑删除字段logic-delete-value: 1和logic-not-delete-value: 0是指定删除标识值。实体字段上的TableLogic其实可以用全局字段名替代但建议两者配合避免个别表字段名不一致导致漏配置。逻辑删除开启后MP 会自动改写 SQLdeleteById(id)变成UPDATE user SET deleted 1 WHERE id ? AND deleted 0selectById(id)变成SELECT ... FROM user WHERE id ? AND deleted 0selectList(wrapper)自动追加AND deleted 0这个自动改写是透明的SQL 日志里能看到。但要注意如果表里没有deleted字段而全局配置了逻辑删除那所有 SQL 都会报列不存在的错。如果某些查询需要查已删除数据不能直接用 BaseMapper 方法要写原生 SQL 绕过 MP 的自动逻辑。唯一索引字段在逻辑删除后可能有问题。比如用户名有唯一索引删掉一个用户后同名记录还能插入吗通常会因为deleted字段也参与唯一约束而冲突这已经不属于 MP 的问题而是设计层面的取舍。4.2 字段自动填充实现创建时间、更新时间、操作人这类字段业务代码里经常要重复 set容易漏。MP 的自动填充需要两步第一步实体字段上加TableField(fill FieldFill.INSERT)或UPDATETableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;第二步实现MetaObjectHandler接口注册成 BeanComponent 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()); } }这里有个很隐蔽的坑如果你用strictInsertFill它只会填充当前值为 null 的字段。也就是说如果业务代码显式给createTime设置了一个值MP 不会覆盖它。这其实是好事可以应对某些需要回填旧数据的场景。另一个常见问题数据库字段类型是datetime实体字段是LocalDateTime自动填充没问题如果实体用了Date则是java.util.Date和LocalDateTime的转换问题容易序列化出错。建议统一使用LocalDateTime并且在 Jackson 配置里定义好格式化避免接口返回的时间格式变成数组。4.3 乐观锁插件配置与注意点并发更新时最简单安全的做法是乐观锁。MP 的乐观锁实现方式是通过版本号字段必须在插件列表里加OptimisticLockerInnerInterceptorinterceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());注意插件顺序。分页插件和乐观锁插件可以并存但如果是逻辑删除插件需要在分页之前。MP 3.5.x 的插件顺序很重要顺序不对可能导致 SQL 改写不生效。实体类里加上版本字段Version private Integer version;乐观锁生效的条件有严格限制不能使用UpdateWrapper设置版本字段为 null 或固定值。updateById时必须把带 version 的实体传进去MP 执行的 SQL 会带上WHERE version 旧值。更新完成后M 不会自动回填新的 version 到实体里你需要从之前读取的实体或重新查询来获取。实际使用中我踩过一个坑批量更新时比如update(entity, wrapper)如果 wrapper 没显式携带 version 条件乐观锁可能不生效。后来查文档发现update(entity, wrapper)形式的更新不会像updateById那样自动拼接 version需要自己控制或者避免在这种场景下用乐观锁。5. 实际操作中的常见问题与排查5.1 常见异常速查表我把实际项目里遇到的、以及社区里常见的问题整理成了表格方便排查异常现象常见原因解决思路启动报Invalid bound statement (not found)Mapper XML 没有扫到检查mapper-locations路径和 XML namespace 是否匹配控制台打印 SQL 报Unknown column xxx实体属性名和数据库字段映射不一致开启 camel-case 映射或使用TableField显式指定insert后主键是一串雪花 ID数据库自增但配置没有设置为auto在id-type中改为auto或实体TableId(type IdType.AUTO)逻辑删除后查询还出现已删数据没加TableLogic或全局配置没生效检查实体字段注解和global-config.db-config.logic-delete-field分页 total 为 0分页插件未正确配置检查MybatisPlusInterceptor是否注入版本是否匹配updateById字段更新为 null 不生效MP 默认只更新非空字段使用UpdateWrapper.set(字段, null)乐观锁并发更新总是成功插件未加载或更新方法不满足条件确认插件加入并使用updateById且实体中带 version 字段多次修改实体属性后selectById缓存了旧值一级缓存重复使用关闭一级缓存或使用新SqlSession一般不会影响业务但要注意5.2 我个人踩过的坑与解决思路第一个坑是版本号问题。当时我从网上抄了一份 MyBatis-Plus 3.4.2 的配置项目用的 Spring Boot 2.5结果启动就报 class not found。排查到最后发现是 mybatis-plus 和 mybatis-plus-generator 的版本不一致一个 3.4.2 一个 3.5.0maven 依赖调解后内部类版本乱了。后来我把所有 MP 相关的依赖都统一到mybatis-plus-boot-starter上由它传递依赖问题迎刃而解。所以版本管理这件事不能只盯着核心包starter、generator、extension 必须一致。第二个坑是分页插件顺序。某次加逻辑删除后deleteById没有自动改写log 里打印的还是 delete SQL。后来发现我只加了PaginationInnerInterceptor和OptimisticLockerInnerInterceptor漏了BlockAttackInnerInterceptor不对是逻辑删除其实不需要单独插件靠的是TableLogic。那次问题出在TableLogic的字段名全局配置覆盖掉了实体注解导致字段识别错了。把global-config.db-config.logic-delete-field和实体字段对齐后就好了。第三个坑是字段映射。有一个表字段叫is_deleted实体属性叫deleted我没加TableField默认驼峰映射把它映射成了isDeleted结果所有查询都报字段不存在。后来我给实体字段加了TableField(is_deleted)明确指定数据库列名才算彻底解决。业务里碰到is_开头的字段真的建议显式注解不要依赖默认映射。第四个坑其实是设计层面的逻辑删除加唯一索引的问题。用户表用户名有唯一索引我先删了一个用户再插入一个同名用户因为逻辑删除只是把 deleted 置为 1唯一索引仍然存在导致插入失败。最后我调整了表结构把唯一索引改成“用户名 deleted”的组合索引同时业务层在逻辑删除时把 deleted 改成主键 ID 的大数字这样既保留唯一性又能支持重名插入。这个方案不一定适合所有场景但如果你也有逻辑删除加唯一索引的需求可以参考。写在最后的实际操作体会这篇笔记写到这我回想了一下MyBatis-Plus 的“简单配置”其实分两层第一层是启动即用的默认配置第二层是业务化定制比如逻辑删除、乐观锁、自动填充。很多人只停留在第一层觉得 MP 就是一个少写 SQL 的工具遇到奇怪问题就束手无策。实际上把配置背后的改写逻辑搞明白比死记 API 有用得多。我自己的经验是遇到 MP 的诡异行为第一件事不是查 API而是把配置项和 SQL 日志对照起来看。比如逻辑删除有没有生效看打印的 SQL 里有没有WHERE ... AND deleted 0分页有没有生效看 SQL 里有没有 LIMIT自动填充有没有生效看 INSERT 语句里有没有带上创建时间。只要愿意看 SQL绝大多数问题都能快速定位。最后再分享一个小技巧学习 MP 的时候不要直接用官方生成器自动生成所有代码建议先手动写一遍实体类、Mapper、配置和简单的 CRUD。这个“笨办法”会逼你搞清楚每个配置的含义以后再遇到复杂项目就能很快判断哪些地方需要改配置、哪些地方需要写原生 SQL。很多人用工具生成完代码一脸懵遇到问题完全不知道从哪下手就是少了这一步亲手配置的过程。希望这篇笔记能帮你在 MyBatis-Plus 的使用上少走一些弯路。
延伸阅读

更多相关文章

2026/10/14 6:16:18

SSM框架就业招聘系统毕业设计:数据库设计与功能实现全解析

1. 项目概述与功能拆解做毕业设计这些年,我接触最多的一个类型就是“XX招聘系统”“XX就业平台”,而SSM(Spring SpringMVC MyBatis)版本的就业招聘查询系统,算得上是这类题目里的“常青树”。我之所以说它是常青树&a…

2026/10/14 6:16:18

适配器模式实战:类适配器与对象适配器的选型与实现

写这篇的时候,我正在帮一个老系统做接口迁移。系统里有个日志模块,原本只负责把日志写到本地文件,现在要对接新的统一日志平台,走HTTP上报。按常规思路,得改老模块的代码,可这个模块被好几个业务方直接依赖…

2026/10/14 7:36:21

失物招领小程序开发:从信息模型到智能匹配算法设计

1. 失物招领的最大痛点不是“没功能”,而是“数据孤岛”做小程序失物招领系统之前,我其实一度很犹豫。这题目听上去太“经典”了,随便搜一下就能找到上百篇差不多的毕设论文,功能无非是发布丢失物品、发布捡到物品、列表展示、条件…

2026/10/14 7:36:21

RTMP connect后三步关键握手:createStream、publish与setChunkSize详解

1. 项目概述:为什么“connect后续三剑客”是RTMP协议落地的生死线你打开一个直播推流工具,点下“开始推流”,几秒后画面就出现在播放器里——这个过程看似简单,背后却藏着RTMP协议最精密、最不容出错的一组握手序列。标题里说的“…

2026/10/14 7:36:21

aardio中获取与使用进程PID的全面指南

1. 理解PID的基本概念在Windows系统中,每个正在运行的程序都会被分配一个唯一的数字标识符,这就是我们常说的进程ID(Process ID,简称PID)。这个PID对于系统管理、进程间通信、资源监控等操作至关重要。想象一下&#x…

2026/10/14 7:36:21

开源AI Agent框架WorkBuddy实战:从部署到构建数字员工助手

最近几天陆陆续续有读者在后台催我聊聊 WorkBuddy,说又被刷屏了。我也趁着周末仔细测了一下,坦率讲,这个开源项目确实是最近 AI Agent 赛道上最值得折腾的一个。它来自国内某头部互联网大厂,开源版 WorkBuddy 说白了就是一套 AI 智…

2026/10/14 7:36:21

Photoshop CS6免安装版实战:解压部署、性能调优与避坑指南

简介:Photoshop CS6 解压即用版面向设计师、摄影师及图像编辑爱好者,无需安装流程即可直接调用软件核心功能,解决临时或轻量环境下对专业修图工具的快速需求。压缩包共601个文件,以83个dll运行库、39个jsx脚本、29个xml配置和52个…

2026/10/14 7:31:21

AnyPS5技术解析:PS5跨设备接入的可行性边界与工程实践

项目标题:“AnyPS5”这个名称本身带有强烈的指向性与模糊性并存的特征——它既像一个技术代号,又像一句口号;既暗示与PlayStation 5生态相关,又刻意规避了“官方”“模拟”“兼容”等敏感定性词。作为从业十多年、经手过上百个跨平…

2026/10/13 5:19:27

Python调用Gemini Structured Outputs实现工单路由门禁

客服工单最怕的不是模型“答错一句话”,而是它给出一段看起来合理的说明,程序却从中猜错优先级。通俗做法是:要求模型只交 JSON(JavaScript Object Notation,轻量数据格式),再让代码验证它。Gem…

2026/10/13 17:09:18

Spring Boot超市进销存系统毕设实战:从需求拆解到答辩通关

最近带的一个学生项目组里,有A同学跑来问我:选什么毕设题目最稳妥,既能让评审老师觉得工作量够,又不会在答辩时被问到语无伦次。我第一反应就是推荐基于Spring Boot的超市仓库管理系统——也就是超市进销存系统。这个题目乍一看平…

2026/10/14 2:32:55

Flutter StatefulWidget 生命周期核心解析

很多刚开始接触 Flutter 的朋友,在看完一堆“Hello World”和基础组件之后,大概率都会撞上同一堵墙:StatefulWidget 里那堆 initState、build、dispose 方法,到底什么时候被调用?为什么顺序是那样?在里面到…

2026/10/14 0:00:33

DFS算法解决飞机降落调度问题实战

1. 项目背景与核心思路飞机降落问题是一个经典的调度优化场景,也是算法竞赛和面试中的高频题目。这个题目要求我们合理安排多架飞机的降落顺序和时间,确保所有飞机都能安全着陆而不发生冲突。在实际航空管制中,这类问题直接关系到飞行安全和机…

2026/10/14 0:00:33

macOS/iOS高精度计时:mach_absolute_time原理与实战

1. 项目概述:为什么mach_absolute_time是 macOS/iOS 时间测量的“黄金标准”在 macOS 和 iOS 开发中,我见过太多人用CFAbsoluteTimeGetCurrent()或NSDate.timeIntervalSinceReferenceDate做性能打点,结果在高精度场景下被系统时钟跳变、NTP 同…

2026/10/14 0:00:33

SCORM课件演示与测试全解析:从API通信到自动化验证

简介:这是一套遵循SCORM标准的交互式课件演示与测试资源,面向E-Learning课程开发者、LMS平台测试人员及在线教学从业者,用于验证课件在不同学习管理系统中的识别、传递与跟踪能力。压缩包共28个文件,约12.07MB,以swf动…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑