1. MyBatis-Plus更新策略深度解析
MyBatis-Plus作为MyBatis的增强工具包,在数据库操作层面提供了诸多便捷功能。其中字段更新策略是日常开发中最容易遇到问题的场景之一。默认情况下,MyBatis-Plus会忽略值为null的字段更新,这个设计背后有着合理的考量:
- 数据安全性:防止前端传递null意外覆盖数据库已有值
- 性能优化:减少不必要的SQL字段更新
- 业务合理性:null通常表示"无意义"而非"要清空"
这种策略在3.5.17版本中依然保持,对应的Spring Boot版本通常为2.7.x系列。理解这个机制需要先了解FieldStrategy枚举类,它定义了五种字段策略:
public enum FieldStrategy { IGNORED, // 忽略判断 NOT_NULL, // 非NULL判断 NOT_EMPTY, // 非空判断 DEFAULT, // 跟随全局配置 NEVER // 不加入SQL }关键提示:在3.4.0+版本中,NOT_EMPTY对字符串还会检查blank状态
2. 四种解决方案实战指南
2.1 全局配置方案
在application.yml中设置全局策略:
mybatis-plus: global-config: db-config: update-strategy: ignored这种方式的优缺点非常明显:
- 优点:配置简单,一劳永逸
- 缺点:影响所有更新操作,可能带来安全隐患
实测发现,在3.5.17版本中,这种配置会影响BaseMapper的updateById方法,但对UpdateWrapper不产生作用。如果项目中使用多种更新方式,需要特别注意这点。
2.2 字段注解方案
在实体类字段上使用@TableField注解:
@TableField(updateStrategy = FieldStrategy.IGNORED) private String phoneNumber;这种方案更加精准可控:
- 只对特定字段生效
- 支持不同字段采用不同策略
- 与全局配置可以组合使用
踩坑记录:当字段同时设置insertStrategy和updateStrategy时,更新操作只会读取updateStrategy配置
2.3 UpdateWrapper动态方案
使用UpdateWrapper进行条件更新:
UpdateWrapper<User> wrapper = new UpdateWrapper<>(); wrapper.set("email", null) .eq("id", 1); userMapper.update(null, wrapper);这种方法的特点是:
- 完全绕过实体类字段策略
- SQL直接生成SET email=null
- 需要手动处理字段名映射问题
特别注意:在3.5.x版本中,如果wrapper.set()传入的是实体对象,仍然会受到字段策略影响。
2.4 自定义SQL方案
在Mapper.xml中编写完整SQL:
<update id="updateWithNull"> UPDATE user SET username=#{username}, phone=#{phone,jdbcType=VARCHAR}, email=NULL WHERE id=#{id} </update>这种方案的适用场景:
- 需要精确控制个别字段行为
- 复杂更新逻辑
- 性能敏感型操作
3. 深度对比与选型建议
3.1 方案对比表
| 方案 | 精确度 | 侵入性 | 维护成本 | 适用场景 |
|---|---|---|---|---|
| 全局配置 | 低 | 低 | 低 | 快速原型开发 |
| 字段注解 | 中 | 中 | 中 | 常规业务开发 |
| UpdateWrapper | 高 | 高 | 高 | 动态条件更新 |
| 自定义SQL | 最高 | 最高 | 最高 | 复杂业务逻辑 |
3.2 版本兼容性注意事项
不同MyBatis-Plus版本存在行为差异:
- 3.0.x:FieldStrategy枚举值较少
- 3.4.x:引入NOT_EMPTY对字符串的blank检查
- 3.5.x:优化了UpdateWrapper的处理逻辑
在Spring Boot集成时要注意:
- 2.5.x → MyBatis-Plus 3.4.x
- 2.7.x → MyBatis-Plus 3.5.x
- 3.0.x → MyBatis-Plus 最新版
4. 生产环境实战经验
4.1 分页查询的特殊情况
当使用分页查询结合更新操作时:
Page<User> page = new Page<>(1, 10); page.addOrder(OrderItem.asc("create_time")); // 注意:null值排序在不同数据库表现不同 userMapper.selectPage(page, queryWrapper);常见问题:
- MySQL中NULL在ASC排序时最先出现
- Oracle需要NVL函数处理
- 分页后再更新可能导致数据不一致
4.2 批量更新性能优化
对于批量更新null值的场景:
// 错误示范 - N+1问题 list.forEach(item -> userMapper.updateById(item)); // 正确做法 - 批量更新 userMapper.updateBatchById(list);性能对比测试(1000条数据):
- 单条更新:约1200ms
- 批量更新:约200ms
- 注意:批量更新需要开启事务
4.3 JSON字段处理技巧
当处理包含null的JSON字段时:
@TableField(typeHandler = FastjsonTypeHandler.class) private JSONObject extInfo;使用Hutool工具类转换时:
JSONUtil.parseObj("{\"key\":null}"); // 注意:这里的null会被保留5. 常见问题排查指南
5.1 问题现象:更新不生效
排查步骤:
- 检查MyBatis-Plus版本
- 确认全局配置是否加载
- 查看实体类注解
- 检查SQL日志输出
5.2 问题现象:NPE异常
可能原因:
- 实体类字段未加@TableField注解
- 使用LambdaUpdateWrapper时方法引用错误
- 类型处理器不匹配
解决方案:
// 使用Wrapper时要判空 wrapper.set(ObjectUtil.isNotNull(value), "column", value);5.3 问题现象:字段被意外更新
防护措施:
- 使用@TableField(update=false)
- 在Wrapper中明确指定字段
- 启用SQL拦截器审计
6. 高级技巧与最佳实践
6.1 动态策略切换方案
通过自定义MetaObjectHandler实现:
public class CustomMetaObjectHandler implements MetaObjectHandler { @Override public void updateFill(MetaObject metaObject) { if (needUpdateNull()) { this.strictUpdateFill(metaObject, "field", String.class, null); } } }6.2 多租户场景下的处理
在Saas系统中要注意:
- 租户ID字段永远不能为null
- 使用自动填充避免遗漏
- 更新前校验租户权限
6.3 审计字段的特殊处理
对于createTime/updateTime等字段:
@TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime;即使设置为null,自动填充也会覆盖这个值