别再 new 实体类了!MyBatis-Plus UpdateWrapper 更新方式的避坑指南与最佳实践

在这里插入图片描述

📌 开篇:一次 Code Review 引发的争论

上周组内 Code Review,同事小李提交了一段逻辑删除的代码:

// 方式 B:先创建实体对象,再配合 UpdateWrapper
TeachClass teachClass = new TeachClass();
teachClass.setDeleteFlag(Constants.DELETE_FLAG_DELETED);
teachClass.setDeletedBy(userId);
teachClass.setDeletedTime(now);
UpdateWrapper<TeachClass> updateWrapper = new UpdateWrapper<>();
updateWrapper.lambda()
    .eq(TeachClass::getInstanceId, instanceId)
    .eq(TeachClass::getDeleteFlag, Constants.DELETE_FLAG_UNDELETED);
teachClassMapper.update(teachClass, updateWrapper);

“这不挺好吗?创建实体对象赋值,然后条件更新。” 一位同事说。

“其实有更优雅的方式,而且这个写法藏着一个小坑。” 我指着屏幕。

这段代码最终被改成了这样:

// 方式 A(推荐):直接使用 LambdaUpdateWrapper 设置字段
LambdaUpdateWrapper<TeachClass> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(TeachClass::getInstanceId, instanceId)
             .eq(TeachClass::getDeleteFlag, Constants.DELETE_FLAG_UNDELETED)
             .set(TeachClass::getDeleteFlag, Constants.DELETE_FLAG_DELETED)
             .set(TeachClass::getDeletedBy, userId)
             .set(TeachClass::getDeletedTime, now);
teachClassMapper.update(null, updateWrapper);

今天我们就来深入聊聊这两种写法的差异,以及为什么我强烈推荐第二种。


1️⃣ 两种更新方式的业务场景

假设我们有一个教学班表 teach_class,需要根据 instanceId 逻辑删除所有未被删除的记录(delete_flag = 0 表示未删除)。更新字段包括:删除标记、删除人、删除时间。

🔹 方式 B(创建实体对象 + UpdateWrapper)

TeachClass teachClass = new TeachClass();
teachClass.setDeleteFlag(Constants.DELETE_FLAG_DELETED);
teachClass.setDeletedBy(userId);
teachClass.setDeletedTime(now);
UpdateWrapper<TeachClass> updateWrapper = new UpdateWrapper<>();
updateWrapper.lambda()
    .eq(TeachClass::getInstanceId, instanceId)
    .eq(TeachClass::getDeleteFlag, Constants.DELETE_FLAG_UNDELETED);
teachClassMapper.update(teachClass, updateWrapper);

🔹 方式 A(仅用 LambdaUpdateWrapper,不创建实体)

LambdaUpdateWrapper<TeachClass> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.eq(TeachClass::getInstanceId, instanceId)
             .eq(TeachClass::getDeleteFlag, Constants.DELETE_FLAG_UNDELETED)
             .set(TeachClass::getDeleteFlag, Constants.DELETE_FLAG_DELETED)
             .set(TeachClass::getDeletedBy, userId)
             .set(TeachClass::getDeletedTime, now);
teachClassMapper.update(null, updateWrapper);

2️⃣ 生成的 SQL 语句对比

我们先看看两种方式最终会生成什么 SQL。

方式 B 的 SQL(大致)

UPDATE teach_class 
SET delete_flag = 1, deleted_by = 1001, deleted_time = '2025-02-28 10:00:00'
WHERE instance_id = 123 AND delete_flag = 0

方式 A 的 SQL(完全相同)

UPDATE teach_class 
SET delete_flag = 1, deleted_by = 1001, deleted_time = '2025-02-28 10:00:00'
WHERE instance_id = 123 AND delete_flag = 0

结论:从最终执行的 SQL 角度看,两者完全等价。MyBatis-Plus 会将实体对象中非 null 的属性拼接到 SET 子句中,而 UpdateWrapper 的 .set() 方法也是做同样的事。那么差异在哪?


3️⃣ 深入原理:FieldStrategy 与 null 值处理

⚠️ 方式 B 的隐藏风险

假设业务需求变化:我们需要将某条记录的 deleted_by 字段清空(设为 null)。用方式 B 怎么做?

teachClass.setDeletedBy(null);       // 希望更新为 null
teachClass.setDeleteFlag(...);
// 其余不变

运行后发现 deleted_by 根本没被更新!为什么?

因为 MyBatis-Plus 默认的字段策略是 FieldStrategy.NOT_NULL只有实体对象中值不为 null 的字段才会参与生成 SET 子句deleted_by 为 null 就被直接忽略了。

🛠️ 方式 A 如何解决?

使用 LambdaUpdateWrapper 的 .set() 方法,你可以显式指定要更新为 null

updateWrapper.set(TeachClass::getDeletedBy, null);  // 强制 SET deleted_by = NULL

.set() 方法不受 FieldStrategy 影响,它会忠实生成你指定的 SQL 片段。

🔬 源码窥探(MyBatis-Plus 3.5+)

com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper 中,.set() 方法最终会向 sqlSet 集合中添加一条 column = #{el} 的表达式,这个表达式会在后续的 SQL 拼接中直接使用。而实体对象方式的 SQL 生成则由 TableInfo 根据字段策略过滤后生成。

📌 换句话说:方式 B 的更新能力受实体对象字段值的限制,而方式 A 可以自由控制任何字段的更新值,包括 null。


4️⃣ 优缺点对比表

维度 方式 B(创建实体对象) 方式 A(仅用 LambdaUpdateWrapper)
代码简洁性 需要额外创建对象,代码行数多 链式调用,一行搞定所有更新
字段更新自由度 ❌ 无法更新 null 字段(除非修改全局策略) ✅ 可显式更新任意值,包括 null
类型安全 ✅ Lambda 条件部分安全,但实体字段赋值是普通 Java ✅ 全链路 Lambda,完全类型安全
可读性 需要理解“实体对象负责 SET,Wrapper 负责 WHERE” 更新字段和条件都在 Wrapper 中,一目了然
维护性 字段名变化时,实体类字段同步改,IDE 能感知 字段名变化时,所有 .set() 调用也由 IDE 检查
适用场景 仅适用于“已有实体对象”且无需更新 null 的场景 所有场景,尤其条件更新、动态字段更新

5️⃣ 最佳实践总结

  1. 在 MyBatis-Plus 中,凡是需要根据条件更新记录的场景,一律优先考虑 LambdaUpdateWrapper + .set() 方式。
  2. 不要为了更新几个字段而创建实体对象,除非你已经有现成的实体对象(比如刚从数据库查出来),并且更新逻辑简单且不涉及 null 值。
  3. 如果需要将某字段更新为 null,使用 .set(字段, null) 即可,无需调整全局 FieldStrategy
  4. 始终使用 LambdaUpdateWrapper 而非原始的 UpdateWrapper,以获得编译期类型检查。

6️⃣ 最终代码示例(最佳实践)

// 逻辑删除:根据 instanceId 删除所有未删除的教学班
public void logicDeleteByInstanceId(Long instanceId, Long userId) {
    LambdaUpdateWrapper<TeachClass> updateWrapper = new LambdaUpdateWrapper<>();
    updateWrapper.eq(TeachClass::getInstanceId, instanceId)
                 .eq(TeachClass::getDeleteFlag, Constants.DELETE_FLAG_UNDELETED)
                 .set(TeachClass::getDeleteFlag, Constants.DELETE_FLAG_DELETED)
                 .set(TeachClass::getDeletedBy, userId)
                 .set(TeachClass::getDeletedTime, LocalDateTime.now());
    // 需要清空某个字段?直接 .set(字段, null)
    // updateWrapper.set(TeachClass::getRemark, null);
    
    teachClassMapper.update(null, updateWrapper);
}

📌 结语

回到开头的 Code Review,我们最终选择了方式 A,不仅代码更简洁,而且为未来的变更(比如清空字段)留足了余地。MyBatis-Plus 提供了强大的 Wrapper 体系,学会合理使用,能让你的代码既健壮又优雅。

如果你还在创建实体对象来做条件更新,不妨试试直接使用 LambdaUpdateWrapper,相信你会回来感谢我的。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐