MyBatis‑Plus线上高频踩坑:本地一切正常,上线后出现更新丢失、SQL异常、性能灾难
MyBatis‑Plus线上高频踩坑:本地一切正常,上线后出现更新丢失、SQL异常、性能灾难
从事后端开发,MyBatis‑Plus几乎是国内Java项目的标配ORM工具。封装了大量通用CRUD,省去手写大量重复SQL,开发效率提升非常明显。
但很多开发者只是简单看过API文档,直接复制示例代码投入业务使用,忽略框架底层行为。经常出现一种现象:本地单元测试、开发环境测试全部正常,一旦上生产,伴随真实数据、高并发,就暴露出各种诡异问题:数据库更新丢失、无条件全表更新、SQL性能恶化、字段映射异常。
这类问题大部分不是框架Bug,而是开发者对API理解不到位,写出了存在隐患的业务代码。下面全部来自真实项目故障复盘,包含错误代码、故障现象、根因分析、生产环境可用修复方案,适合后端开发日常自查,也可以作为团队内部避坑参考。

坑点1:updateById传入实体部分字段为null,误把数据库字段更新为null
故障现象
业务只希望更新部分字段,实体对象中其他字段没有赋值为null。调用updateById()之后,数据库中非目标字段被置为null,原有业务数据直接丢失。本地测试很难复现,很多时候测试用例只赋值目标字段,没有关注其他字段。
❌错误代码
java
//只更新用户手机号,其他字段没有赋值
User user = new User();
user.setId(1001L);
user.setPhone("13800001111");
//错误:会把对象内部为null的属性,更新到数据库
userMapper.updateById(user);
根因:很多新手误以为
updateById只会更新set过的字段。如果实体对象属性为null,在默认策略下,MyBatis‑Plus会把null字段拼入update语句,执行set phone=?,name=null,age=null ... where id=?,直接覆盖原有数据库数据,造成业务数据丢失。
✅修复方案
方案1:使用LambdaUpdateWrapper,只设置需要更新的字段,避免实体对象null属性干扰。
java
LambdaUpdateWrapper
wrapper.eq(User::getId,1001L)
.set(User::getPhone,"13800001111");
userMapper.update(null,wrapper);
方案2:配置全局策略,insertStrategy、updateStrategy设置为NOT_NULL,但是全局配置有风险,部分业务场景确实需要主动置null,全局修改容易引发新问题,业务优先推荐Wrapper写法。
实战提醒:绝对不要new一个实体,只set少量字段就直接updateById,这是线上数据丢失高频来源。
坑点2:lambda条件查询误用,条件对象复用,出现条件叠加、查询条件错乱
故障现象
同一个LambdaQueryWrapper对象,在循环、多次业务逻辑中复用。后面的查询会带上上一次残留的查询条件,查询结果不符合预期,时而正常时而异常,属于偶现bug。
❌错误代码
java
LambdaQueryWrapper
//第一次查询
wrapper.eq(User::getStatus,1);
List
//第二次查询,想只按name查询,没有清空wrapper
wrapper.eq(User::getName,"张三");
List
//最终条件变成 status=1 AND name='张三',并不是预期只按name查询
根因:Wrapper对象是可变对象,eq、like这类方法会不断向对象内部追加条件。对象不复用、不重置,旧条件会累积。很多人图省事复用同一个wrapper实例,本地测试场景简单不容易暴露,复杂业务逻辑就出现查询错乱。
✅修复方案
- 每一次查询,新建独立的Wrapper实例,不要复用;
- 如果必须复用,调用
wrapper.clear()清空所有条件。
java
//每次查询新建对象
LambdaQueryWrapper
wrapper2.eq(User::getName,"张三");
List
坑点3:使用or()不使用括号,SQL优先级错误,查询结果完全不符合预期
故障现象
拼接多条件查询,业务期望:status=1 AND (name='张三' OR phone='138xxxx')。
代码写完运行,生成SQL变成(status=1 AND name='张三') OR phone='138xxxx',逻辑完全跑偏,查询出来大量无关数据。
❌错误代码
java
LambdaQueryWrapper
wrapper.eq(User::getStatus,1)
.or()
.eq(User::getName,"张三")
.eq(User::getPhone,"13800001111");
List
根因:MyBatis‑Plus的
or()不会自动把后面的条件整体加上括号。SQL的and优先级高于or,不手动分组,条件逻辑会完全和业务预期不一致。这个坑出现频率极高,很多人本地测试刚好命中数据,没有发现逻辑错误,上线后业务数据变多,暴露出查询逻辑错误。
✅修复方案,使用嵌套wrapper实现括号分组
java
LambdaQueryWrapper
wrapper.eq(User::getStatus, 1)
.and(w -> w.eq(User::getName, "张三").or().eq(User::getPhone, "13800001111"));
List
坑点4:分页查询不传total,大数据量下count性能爆炸
故障现象
使用Page对象做分页,传入pageNum、pageSize,没有关闭count统计。当表数据量大,MyBatis‑Plus自动执行select count(1) from table统计总数。大表没有合适索引,count查询十分缓慢,拖垮整个接口,数据库CPU飙升。
❌错误代码
java
//默认会执行count统计,大表性能灾难
Page
IPage
根因:默认分页会自动执行count获取总条数。如果业务不需要前端展示总页数,只需要下拉滚动加载,完全不需要count,大表count代价极高。
✅修复方案
java
Page
IPage
补充:必须要count的场景,要保证count对应的SQL有合适索引,避免全表扫描。
坑点5:saveOrUpdate踩坑:主键没有赋值,变成插入;主键存在,变成更新,业务逻辑混乱
故障现象
调用saveOrUpdate,业务本意是有记录就更新,没有就新增。但是前端传入id为null,直接变成插入一条新数据;或者数据库存在记录,但是传入id被错误修改,覆盖别的行数据。
❌错误代码
java
User user = new User();
user.setName("李四");
user.setAge(22);
//id为null,直接执行insert,而不是更新
userService.saveOrUpdate(user);
根因:
saveOrUpdate逻辑:主键不为null,执行updateById;主键为null,执行insert。很多业务直接接收前端DTO转实体,id可能为null,开发者误以为框架会自动按业务唯一字段判断是否存在,实际上只会判断主键id。
✅修复方案
- 理解底层逻辑:saveOrUpdate只判断主键,不会按业务唯一字段去查询判断;
- 如果需要按照手机号、账号等业务唯一字段判断是否存在,必须自己手写查询判断,不能直接依赖saveOrUpdate;
- 业务场景区分清楚,确定id来源,不要直接把前端DTO直接丢进saveOrUpdate。
坑点6:逻辑删除使用不当,自定义where条件忘记带上逻辑删除条件
故障现象
开启MyBatis‑Plus逻辑删除,使用@TableLogic注解。框架自带的select、update会自动拼接deleted=0。但是自己手写自定义XML SQL,忘记手动增加逻辑删除条件,查询出来已经被逻辑删除的数据,业务出现已删除数据又被查询出来的诡异bug。
根因:
@TableLogic只对MP内置的CRUD方法生效。自己写在xml里面的自定义SQL,框架不会自动追加逻辑删除条件。很多开发误以为开启全局逻辑删除,所有SQL都会自动过滤已删除数据。
✅修复方案
- 自定义SQL手动拼接条件
where deleted = 0; - 注意逻辑删除字段更新、查询都要带上条件,否则已删除数据会被业务读到。
线上开发总结:MyBatis‑Plus编码的几条实战准则
- updateById尽量不要new实体只赋值少量字段,优先使用LambdaUpdateWrapper;
- Wrapper属于可变对象,禁止跨逻辑、循环复用,每次查询新建实例;
- or条件一定要注意SQL优先级,复杂条件使用and嵌套wrapper做括号分组;
- 大表分页,不需要总条数就关闭count,避免大表count带来性能问题;
- 彻底搞懂saveOrUpdate逻辑,它只判断主键id,不会按照业务字段查重;
- @TableLogic逻辑删除仅对内置CRUD生效,自定义xmlSQL必须手动补充条件;
- 不要过度依赖框架封装,一定要看懂最终生成的SQL,打印SQL日志核对真实执行语句。
框架只是工具,它可以简化开发,但不会帮你规避业务逻辑错误。很多线上故障,并不是框架不好,而是没有吃透底层行为,把示例代码直接照搬进生产。本地测试数据简单,很多问题不会暴露,只有线上真实数据才会显现。遇到诡异问题,优先打印观察最终执行SQL,很多问题看SQL一眼就能定位。
友情链接:
凡尘博客、
凡尘影院、
凡尘乡音|凡尘街坊、
凡尘博客|雨落凡尘博客|羽落凡尘博客、
凡尘博客|雨落凡尘博客|羽落凡尘博客
凡尘版权
本文原创技术文章,可发布博客园、网易号,转载请完整保留版权与友链。

MyBatis‑Plus线上高频踩坑:本地一切正常,上线后出现更新丢失、SQL异常、性能灾难
从事后端开发,MyBatis‑Plus几乎是国内Java项目的标配ORM工具。封装了大量通用CRUD,省去手写大量重复SQL,开发效率提升非常明显。
但很多开发者只是简单看过API文档,直接复制示例代码投入业务使用,忽略框架底层行为。经常出现一种现象:本地单元测试、开发环境测试全部正常,一旦上生产,伴随真实数据、高并发,就暴露出各种诡异问题:数据库更新丢失、无条件全表更新、SQL性能恶化、字段映射异常。
浙公网安备 33010602011771号