别再 new 实体类了!MyBatis-Plus UpdateWrapper 更新方式的避坑指南与最佳实践
别再 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️⃣ 最佳实践总结
- 在 MyBatis-Plus 中,凡是需要根据条件更新记录的场景,一律优先考虑
LambdaUpdateWrapper+.set()方式。 - 不要为了更新几个字段而创建实体对象,除非你已经有现成的实体对象(比如刚从数据库查出来),并且更新逻辑简单且不涉及 null 值。
- 如果需要将某字段更新为 null,使用
.set(字段, null)即可,无需调整全局FieldStrategy。 - 始终使用 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,相信你会回来感谢我的。
更多推荐

所有评论(0)