mybatis-plus中更新null值的问题


前言

本文主要介绍 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 值不设置的。

那为什么通过 UpdateWrapperset 方法就可以设置值呢?

同样取查看 set() 方法的源码:

在这里插入图片描述

看到这行代码已经明了,因为可以看到它是通过 String.format("%s=%s",字段,值) 拼接 sql 的方式,也是是说不管设置了什么值都会是 字段=值 的形式,所以就会被设置上去。


四、解决方式

从上文分析就可以知道已经有两种方式实现更新 null ,不过除此之外就是直接修改全局配置,所以这三种方法分别是:


方式一:修改单个字段策略模式

这种方式在上文已经叙述过了,直接在实体类上指定其修改策略模式即可

@TableField(updateStrategy = FieldStrategy.IGNORED)

在这里插入图片描述

如果某些字段需要可以在任何时候都能更新为 null,这种方式可以说是最方便的了。


方式二:修改全局策略模式

通过刚刚分析源码可知,如果没有指定字段的策略,取的是 DbConfig 中的配置,而 DbConfigGlobalConfig 的静态内部类

在这里插入图片描述

所以我们可以通过修改全局配置的方式,改变 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 进行设置

这种方式前面也提到过了,就是使用 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.updateStrategyFieldStrategy.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 的配置类中将其配置为 springbean 即可:

@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

猜你喜欢

转载自blog.csdn.net/xhmico/article/details/133124891