首页
学习
活动
专区
圈层
工具
发布

MyBatisPlus 使用 saveOrUpdate() 的坑

很多人刚上手 MyBatis-Plus(简称 MP)时,会觉得它特别“省心”——CRUD 操作几乎一行搞定,saveOrUpdate()更是被视为“懒人利器”。它能根据主键是否存在自动决定是insert还是update。但实际用过一段时间后,你可能会发现,这玩意儿藏着不少坑。下面结合我在生产环境踩过的几个例子,聊聊它到底容易出错的点在哪,以及怎么绕过去。

一、表面上很香的saveOrUpdate()

MP 官方文档里是这样说的:

boolean saveOrUpdate(T entity);

作用简单明了:

如果主键不存在 执行INSERT

如果主键存在 执行UPDATE

比如下面这段代码,看起来再正常不过:

User user = new User();

user.setId(1L);

user.setName("张三");

user.setAge(25);

userService.saveOrUpdate(user);

理论上,如果数据库中id = 1的记录存在,就更新;不存在就插入。 问题是,这个“存在”到底是怎么判断的?

二、隐藏逻辑:它是查了数据库的

很多人以为它会直接判断主键是否为 null 来决定 insert 还是 update,其实不是。

saveOrUpdate()底层会先执行一条SELECT:

SELECT id FROM user WHERE id = ?

如果查得到,就执行:

UPDATE user SET name=?, age=? WHERE id=?

否则执行:

INSERT INTO user (id, name, age) VALUES (?, ?, ?)

看出问题了吗? 这意味着——每次调用saveOrUpdate()都要查一次数据库,哪怕你很确定那条记录肯定存在。

对于单条操作没啥,但批量操作或者高并发场景下,这会直接导致性能暴跌。 尤其是那种在定时任务或批处理里循环调用的写法,简直是自杀行为:

for (User user : userList) {

  userService.saveOrUpdate(user);

}

实际执行的 SQL 数量 =2 * userList.size(),先查后改,非常耗时。

三、另一个坑:主键策略导致误判

有些表的主键并不是数据库自增,而是用雪花算法、UUID 或者业务逻辑生成。 这时候,如果你在saveOrUpdate()里传了一个新生成的 ID,但数据库中其实还没有这条数据,MP 会先查一下:

SELECT id FROM user WHERE id = 'xxxx'

查不到,于是它会INSERT。 听起来没问题,但如果你用了分布式 ID 生成算法(比如多个节点同时生成),就有可能插入重复键

举个例子:

User user = new User();

user.setId(IdUtil.getSnowflakeNextId());

user.setName("李四");

userService.saveOrUpdate(user);

当两个服务节点生成了相同的 ID(这事真不是没发生过),saveOrUpdate()的“查 插入”间隙就会出现并发写入,导致主键冲突。

四、更新逻辑不完整:字段丢失问题

默认情况下,saveOrUpdate()使用的是全字段更新

也就是说,即使你的对象里某个字段是null,它也会把数据库里对应的字段更新成null。 例如:

User user = new User();

user.setId(1L);

user.setAge(30);

userService.saveOrUpdate(user);

结果数据库里name字段被更新成了null—— 因为你的User对象没填这个字段。 这在生产环境里是很危险的。

解决办法有两个:

使用@TableField(updateStrategy = FieldStrategy.NOT_NULL)避免空值覆盖。

或者改用updateById()+ 自定义更新逻辑。

例如:

@TableField(updateStrategy = FieldStrategy.NOT_NULL)

private String name;

五、批量更新的“假优化”

MyBatisPlus 还提供了一个批量方法:

boolean saveOrUpdateBatch(Collection<T> entityList);

很多人以为这会批量执行一条 SQL,但其实不是。 它的内部实现是循环执行单条saveOrUpdate(),只是帮你做了个 for 循环。

源码逻辑大概是这样的:

for (T entity : entityList) {

  saveOrUpdate(entity);

}

所以,批量插入/更新千万不要用它!正确姿势应该是:

userService.saveBatch(userList);

或者用:

userService.updateBatchById(userList);

这两个方法才是真正的批量操作。

六、最坑的一种情况:逻辑删除表

如果你的表启用了逻辑删除字段,比如:

@TableLogic

private Integer deleted;

那么当你调用saveOrUpdate()时,它在判断“记录是否存在”时并不会排除逻辑删除的记录。 也就是说,如果数据库里那条记录被逻辑删除了(deleted=1),它还是会被当成“存在”,然后执行更新操作。 这会让逻辑删除的记录被“复活”回来!

示例:

// deleted=1 的数据被误更新成 deleted=0

userService.saveOrUpdate(user);

解决方法只能是:在业务层自己判断逻辑删除状态,不要盲信 MP 的存在判断。

七、推荐的使用方式

能分就分

新增用save()

更新用updateById()

不确定时再用saveOrUpdate()(比如导入同步类业务)

需要部分字段更新时用updateWrapper明确指定更新字段:

userService.update(

  new UpdateWrapper<User>().eq("id", user.getId()).set("age", user.getAge())

);

逻辑删除表要慎用saveOrUpdate()最好是显式判断:

User exist = userMapper.selectById(user.getId());

if (exist == null) {

  userService.save(user);

} else if (exist.getDeleted() == 0) {

  userService.updateById(user);

}

八、最后

saveOrUpdate()是个方便的工具,但它不是万能的。 在小型系统或后台管理类项目里用没问题,但一旦涉及高并发、大批量、逻辑删除、多源数据同步这些场景,就一定要拆开用。

你要清楚它底层到底做了什么,而不是“以为它会自动判断”。

有时候一行“懒人代码”,藏着的 SQL 比你自己写的还多。 而性能瓶颈、并发冲突、字段丢失,全都可能从这里埋下。

所以,真想少踩坑,最好老老实实写 CRUD。

如果你现在项目里有大规模使用saveOrUpdate()的代码,建议你先查下 SQL 日志,看看实际执行的数量——那一瞬间你大概就会明白,为什么线上总是“慢得离谱”。

  • 发表于:
  • 原文链接https://page.om.qq.com/page/ORX_pyIh5htMcEg9sJEkMDVg0
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。
领券