前言
本文主要介绍 mybatis-plus 中常使用的 update 相关方法的区别,以及更新 null 的方法有哪些等。
至于为什么要写这篇文章,首先是在开发中确实有被坑过几次,导致某些字段设置为 null 值设置不上,其次是官方文档对于这块内容并没有提供一个很完善的解决方案,所以我就总结一下。
一、情景介绍
关于 Mybatis-plus 这里我就不多做介绍了,如果之前没有使用过该项技术的可参考以下链接进行了解。
mybatis-plus 官方文档:https://baomidou.com/
我们在使用 mybatis-plus 进行开发时,默认情况下, mybatis-plus 在更新数据时时会判断字段是否为 null,如果是 null 则不设置值,也就是更新后的该字段数据依然是原数据,虽然说这种方式在一定程度上可以避免数据缺失等问题,但是在某些业务场景下我们就需要设置某些字段的数据为 null。
二、方法分析
这里我准备了一个 student
表进行测试分析,该表中仅有两条数据:
mysql> SELECT * FROM student;
+-----+---------+----------+
| id | name | age |
+-----+---------+----------+
| 1 | 米大傻 | 18 |
+-----+---------+----------+
| 2 | 米大哈 | 20 |
+-----+---------+----------+
在 mybatis-plus 中,我们的 mapper 类都会继承 BaseMapper
这样一个类
public interface StudentMapper extends BaseMapper<Student> {
}
进入到 BaseMapper
这个接口可以查看到该类仅有两个方法和更新有关(这里我就不去分析 IService
类中的那些更新方法了,因为那些方法低层最后也是调用了 BaseMapper
中的这两个 update 方法)
所以就从这两个方法入手分析:
- updateById() 方法
@Test
public void testUpdateById() {
Student student = studentMapper.selectById(1);
student.setName("李大霄");
student.setAge(null);
studentMapper.updateById(student);
}
可以看到使用 updateById() 的方法更新数据,尽管在代码中将 age 赋值为 null
,但是最后执行的 sql 确是:
UPDATE student SET name = '李大霄' WHERE id = 1
也就是说在数据库中,该条数据的 name
值发生了变化,但是 age
保持不变
mysql> SELECT * FROM student WHERE id = 1;
+-----+---------+----------+
| id | name | age |
+-----+---------+----------+
| 1 | 李大霄 | 18 |
+-----+---------+----------+
- update() 方法 — UpdateWrapper 不设置属性
恢复 student
表中的数据为初始数据。
@Test
public void testUpdate() {
Student student = studentMapper.selectById(1);
student.setName("李大霄");
student.setAge(null);
studentMapper.update(student, new UpdateWrapper<Student>()
.lambda()
.eq(Student::getId, student.getId())
);
}
可以看到如果 update() 方法这样子使用,效果是和 updateById() 方法是一样的,为 null
的字段会直接跳过设置,执行 sql 与上面一样:
UPDATE student SET name = '李大霄' WHERE id = 1
- update() 方法 — UpdateWrapper 设置属性
恢复 student
表中的数据为初始数据。
因为 UpdateWrapper
是可以去字段属性的,所以再测试下 UpdateWrapper
中设置为 null
值是否能起作用
@Test
public void testUpdateSet() {
Student student = studentMapper.selectById(1);
student.setName("李大霄");
student.setAge(null);
studentMapper.update(student, new UpdateWrapper<Student>()
.lambda()
.eq(Student::getId, student.getId())
.set(Student::getAge, student.getAge())
);
}
从打印的日志信息来看,是可以设置 null
值的,sql 为:
UPDATE student SET name='李大霄', age=null WHERE id = 1
查看数据库:
mysql> SELECT * FROM student WHERE id = 1;
+-----+---------+----------+
| id | name | age |
+-----+---------+----------+
| 1 | 李大霄 | NULL |
+-----+---------+----------+
三、原因分析
从方法分析中我们可以得出,如果不使用 UpdateWrapper
进行设置值,通过 BaseMapper
的更新方法是没法设置为 null
的,可以猜出 mybatis-plus 在默认的情况下就会跳过属性为 null
值的字段,不进行设值。
通过查看官方文档可以看到, mybatis-plus 有几种字段策略:
也就是说在默认情况下,字段策略应该是 FieldStrategy.NOT_NULL
跳过 null
值的
可以先设置实体类的字段更新策略为 FieldStrategy.IGNORED
来验证是否会忽略判断 null
@Data
@EqualsAndHashCode(callSuper = true)
@ApiModel(value="Student对象", description="学生表")
public class Student extends BaseEntity {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "主键ID")
@TableId(value = "id", type = IdType.AUTO)
private Long id;
@ApiModelProperty(value = "姓名")
@TableField(updateStrategy = FieldStrategy.IGNORED) // 设置字段策略为:忽略判断
private String name;
@ApiModelProperty(value = "年龄")
@TableField(updateStrategy = FieldStrategy.IGNORED) // 设置字段策略为:忽略判断
private Integer age;
}
再运行以上 testUpdateById()
和 testUpdate()
代码
从控制台打印的日志可以看出,均执行 sql:
UPDATE student SET name='李大霄', age=null WHERE id = 1
所以可知将字段更新策略设置为: FieldStrategy.IGNORED
就能更新数据库的数据为 null
了
翻阅 @TableField
注解的源码:
可以看到在源码中,如果没有进行策略设置的话,它默认的策略就是 FieldStrategy.DEFAULT
的,那为什么最后处理的结果是使用了 NOT_NULL
的策略呢?
再追进源码中,可以得知每个实体类都对应一个 TableInfo
对象,而实体类中每一个属性都对应一个 TableFieldInfo
对象
进入到 TableFieldInfo
类中查看该类的属性是有 updateStrategy(修改属性策略的)
查看构造方法 TableFieldInfo()
可以看到如果字段策略为 FieldStrategy.DEFAULT
,取的是 dbConfig.getUpdateStrategy()
,如果字段策略不等于 FieldStrategy.DEFAULT
,则取注解类 TableField
指定的策略类型。
点击进入对象 dbConfig
所对应的类 DbConfig
中
可以看到在这里 DbConfig 默认的 updateStrategy
就是 FieldStrategy.NOT_NULL
,所以说 mybatis-plus
默认情况下就是跳过 null
值不设置的。
那为什么通过 UpdateWrapper
的 set
方法就可以设置值呢?
同样取查看 set()
方法的源码:
看到这行代码已经明了,因为可以看到它是通过 String.format("%s=%s",字段,值)
拼接 sql 的方式,也是是说不管设置了什么值都会是 字段=值
的形式,所以就会被设置上去。
四、解决方式
从上文分析就可以知道已经有两种方式实现更新 null
,不过除此之外就是直接修改全局配置,所以这三种方法分别是:
这种方式在上文已经叙述过了,直接在实体类上指定其修改策略模式即可
@TableField(updateStrategy = FieldStrategy.IGNORED)
如果某些字段需要可以在任何时候都能更新为 null
,这种方式可以说是最方便的了。
通过刚刚分析源码可知,如果没有指定字段的策略,取的是 DbConfig
中的配置,而 DbConfig
是 GlobalConfig
的静态内部类
所以我们可以通过修改全局配置的方式,改变 updateStrategy
的策略不就行了吗?
yml
方式配置如下
mybatis-plus:
global-config:
db-config:
update-strategy: IGNORED
注释 @TableField(updateStrategy = FieldStrategy.IGNORED)
恢复 student
表中的数据为初始数据,进行测试。
可以看到是可行的,执行的 sql 为:
UPDATE student SET name='李大霄', age=null WHERE id = 1
但是值得注意的是,这种全局配置的方法会对所有的字段都忽略判断,如果一些字段不想要修改,也会因为传的是 null 而修改,导致业务数据的缺失,所以并不推荐使用。
这种方式前面也提到过了,就是使用 UpdateWrapper
或其子类进行 set
设置,例如:
studentMapper.update(student, new UpdateWrapper<Student>()
.lambda()
.eq(Student::getId, student.getId())
.set(Student::getAge, null)
.set(Student::getName, null)
);
这种方式对于在某些场合,需要将少量字段更新为 null
值还是比较方便,灵活的。
PS:除此之外还可以通过直接在 mapper.xml
文件中写 sql,但是我觉得这种方式就有点脱离 mybatis-plus
了,就是 mybatis
的操作,所以就不列其上。
五、方式扩展
虽然上面提供了一些方法来更新 null 值,但是不得不说,各有弊端,虽然说是比较推荐使用 UpdateWrapper
来更新 null 值,但是如果在某个表中,某个业务场景下需要全量更新 null 值,而且这个表的字段又很多,一个个 set
真的很折磨人,像 tk.mapper
都有方法进行全量更新 null 值,那有没有什么方法可以全量更新?
虽然 mybaatis-plus
没有,但是可以自己去实现,我是看了起风哥:让mybatis-plus支持null字段全量更新 这篇博客,觉得蛮好的,所以整理下作此分享。
- 实现方式一:使用
UpdateWrapper
循环拼接set
提供一个已 set
好全部字段 UpdateWrapper
对象的方法:
public class WrappersFactory {
// 需要忽略的字段
private final static List<String> ignoreList = new ArrayList<>();
static {
ignoreList.add(CommonField.available);
ignoreList.add(CommonField.create_time);
ignoreList.add(CommonField.create_username);
ignoreList.add(CommonField.update_time);
ignoreList.add(CommonField.update_username);
ignoreList.add(CommonField.create_user_code);
ignoreList.add(CommonField.update_user_code);
ignoreList.add(CommonField.deleted);
}
public static <T> LambdaUpdateWrapper<T> updateWithNullField(T entity) {
UpdateWrapper<T> updateWrapper = new UpdateWrapper<>();
List<Field> allFields = TableInfoHelper.getAllFields(entity.getClass());
MetaObject metaObject = SystemMetaObject.forObject(entity);
for (Field field : allFields) {
if (!ignoreList.contains(field.getName())) {
Object value = metaObject.getValue(field.getName());
updateWrapper.set(StringUtils.camelToUnderline(field.getName()), value);
}
}
return updateWrapper.lambda();
}
}
使用:
studentMapper.update(
WrappersFactory.updateWithNullField(student)
.eq(Student::getId,id)
);
或者可以定义一个 GaeaBaseMapper(全局 Mapper)
继承 BaseMapper
,所有的类都继承自 GaeaBaseMapper
,例如:
public interface StudentMapper extends GaeaBaseMapper<Student> {
}
编写 updateWithNullField()
方法:
public interface GaeaBaseMapper<T extends BaseEntity> extends BaseMapper<T> {
/**
* 返回全量修改 null 的 updateWrapper
*/
default LambdaUpdateWrapper<T> updateWithNullField(T entity) {
UpdateWrapper<T> updateWrapper = new UpdateWrapper<>();
List<Field> allFields = TableInfoHelper.getAllFields(entity.getClass());
MetaObject metaObject = SystemMetaObject.forObject(entity);
allFields.forEach(field -> {
Object value = metaObject.getValue(field.getName());
updateWrapper.set(StringUtils.cameToUnderline(field.getName()), value);
});
return updateWrapper.lambda();
}
}
StringUtils.cameToUnderline()
方法
/**
* 驼峰命名转下划线
* @param str 例如:createUsername
* @return 例如:create_username
*/
public static String cameToUnderline(String str) {
Matcher matcher = Pattern.compile("[A-Z]").matcher(str);
StringBuilder builder = new StringBuilder(str);
int index = 0;
while (matcher.find()) {
builder.replace(matcher.start() + index, matcher.end() + index, "_" + matcher.group().toLowerCase());
index++;
}
if (builder.charAt(0) == '_') {
builder.deleteCharAt(0);
}
return builder.toString();
}
使用:
@Test
public void testUpdateWithNullField() {
Student student = studentMapper.selectById(1);
student.setName("李大霄");
student.setAge(null);
studentMapper
.updateWithNullField(student)
.eq(Student::getId, student.getId());
}
- 实现方式二:mybatis-plus常规扩展—实现
IsqlInjector
像 mybatis-plus 中提供的批量添加数据的 InsertBatchSomeColumn
方法类一样
首先需要定义一个 GaeaBaseMapper(全局 Mapper)
继承 BaseMapper
,所有的类都继承自 GaeaBaseMapper
,例如:
public interface StudentMapper extends GaeaBaseMapper<Student> {
}
然后在这个 GaeaBaseMapper
中添中全量更新 null 的方法
public interface StudentMapper extends GaeaBaseMapper<Student> {
/**
* 全量更新null
*/
int updateWithNull(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper<T> updateWrapper);
}
构造一个方法 UpdateWithNull
的方法类
public class UpdateWithNull extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// 处理逻辑
return null;
}
}
之前说过可以设置字段的更新策略属性为:FieldStrategy.IGNORED
使其可以更新 null 值,现在方法参数中有 TableInfo
对象,通过 TableInfo
我们可以拿到所有的 TableFieldInfo
,通过反射设置所有的 TableFieldInfo.updateStrategy
为 FieldStrategy.IGNORED
,然后参照 mybatis-plus
自带的 Update.java
类的逻辑不就行了。
Update.java
源码:
package com.baomidou.mybatisplus.core.injector.methods;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
public class Update extends AbstractMethod {
public Update() {
}
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
SqlMethod sqlMethod = SqlMethod.UPDATE;
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), this.sqlSet(true, true, tableInfo, true, "et", "et."), this.sqlWhereEntityWrapper(true, tableInfo), this.sqlComment());
SqlSource sqlSource = this.languageDriver.createSqlSource(this.configuration, sql, modelClass);
return this.addUpdateMappedStatement(mapperClass, modelClass, this.getMethod(sqlMethod), sqlSource);
}
}
所以 UpdateWithNull
类中的代码可以这样写:
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.core.enums.SqlMethod;
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableFieldInfo;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import java.lang.reflect.Field;
import java.util.List;
/**
* 全量更新 null
*/
public class UpdateWithNull extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// 通过 TableInfo 获取所有的 TableFieldInfo
final List<TableFieldInfo> fieldList = tableInfo.getFieldList();
// 遍历 fieldList
for (final TableFieldInfo tableFieldInfo : fieldList) {
// 反射获取 TableFieldInfo 的 class 对象
final Class<? extends TableFieldInfo> aClass = tableFieldInfo.getClass();
try {
// 获取 TableFieldInfo 类的 updateStrategy 属性
final Field fieldFill = aClass.getDeclaredField("updateStrategy");
fieldFill.setAccessible(true);
// 将 updateStrategy 设置为 FieldStrategy.IGNORED
fieldFill.set(tableFieldInfo, FieldStrategy.IGNORED);
} catch (final NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
}
SqlMethod sqlMethod = SqlMethod.UPDATE;
String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(),
this.sqlSet(true, true, tableInfo, true, "et", "et."),
this.sqlWhereEntityWrapper(true, tableInfo), this.sqlComment());
SqlSource sqlSource = this.languageDriver.createSqlSource(this.configuration, sql, modelClass);
return this.addUpdateMappedStatement(mapperClass, modelClass, this.getMethod(sqlMethod), sqlSource);
}
public String getMethod(SqlMethod sqlMethod) {
return "updateWithNull";
}
}
再声明一个 IsqlInjector
继承 DefaultSqlInjector
public class BaseSqlInjector extends DefaultSqlInjector {
@Override
public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
// 此 SQL 注入器继承了 DefaultSqlInjector (默认注入器),调用了 DefaultSqlInjector 的 getMethodList 方法,保留了 mybatis-plus 自带的方法
List<AbstractMethod> methodList = super.getMethodList(mapperClass);
// 批量插入
methodList.add(new InsertBatchSomeColumn(i -> i.getFieldFill() != FieldFill.UPDATE));
// 全量更新 null
methodList.add(new UpdateWithNull());
return methodList;
}
}
然后在 mybatis-plus
的配置类中将其配置为 spring
的 bean
即可:
@Slf4j
@Configuration
@EnableTransactionManagement
public class MybatisPlusConfig {
...
@Bean
public BaseSqlInjector baseSqlInjector() {
return new BaseSqlInjector();
}
...
}
我写的目录结构大概长这样(仅供参考):
恢复 student
表中的数据为初始数据,进行测试。
测试代码:
@Test
public void testUpdateWithNull() {
Student student = studentMapper.selectById(1);
student.setName("李大霄");
student.setAge(null);
studentMapper.updateWithNull(student,
new UpdateWrapper<Student>()
.lambda()
.eq(Student::getId, student.getId())
);
student.setName(null);
student.setAge(18);
studentMapper.updateById(student);
}
sql 打印如下:
可以看到使用 updateWithNull()
方法更新了 null。
总结
以上就是我对 mybatis-plus
更新 null
值问题做的探讨,结合测试实例与源码分析,算是解释得比较明白了,尤其是最后扩展的两种方法自认为是比较符合我的需求的,最后扩展的那两种方法都在实体类 Mapper 和 mybatis-plus 的 BaseMapper
中间多抽了一层 GaeaBaseMapper
,这种方式我是觉得比较推荐的,增加了系统的扩展性和灵活性。
扩展 MybatisPlus update 更新时指定要更新为 null 的方法:https://blog.csdn.net/qq_36279799/article/details/132585263
让mybatis-plus支持null字段全量更新:https://blog.csdn.net/a807719447/article/details/129008176
Mybatis-Plus中update()和updateById()将字段更新为null:https://www.jb51.net/article/258648.htm
Mybatis-Plus中update更新操作用法:https://blog.csdn.net/weixin_43888891/article/details/131142279
MyBatis-plus源码解析:https://www.cnblogs.com/jelly12345/p/15628277.html