mybatis 您所在的位置:网站首页 属性getSystime的值为null mybatis

mybatis

2024-07-09 12:08| 来源: 网络整理| 查看: 265

文章目录 前言一、情景介绍二、方法分析三、原因分析四、解决方式五、方式扩展总结

前言

本文主要介绍 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 { }

进入到 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() .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() .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 ,不过除此之外就是直接修改全局配置,所以这三种方法分别是:

方式一:修改单个字段策略模式方式二:修改全局策略模式方式三:使用 UpdateWrapper 进行设置

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

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

@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 进行设置

这种方式前面也提到过了,就是使用 UpdateWrapper 或其子类进行 set 设置,例如:

studentMapper.update(student, new UpdateWrapper() .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 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 LambdaUpdateWrapper updateWithNullField(T entity) { UpdateWrapper updateWrapper = new UpdateWrapper(); List 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 { }

编写 updateWithNullField() 方法:

public interface GaeaBaseMapper extends BaseMapper { /** * 返回全量修改 null 的 updateWrapper */ default LambdaUpdateWrapper updateWithNullField(T entity) { UpdateWrapper updateWrapper = new UpdateWrapper(); List 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 { }

然后在这个 GaeaBaseMapper 中添中全量更新 null 的方法

public interface StudentMapper extends GaeaBaseMapper { /** * 全量更新null */ int updateWithNull(@Param(Constants.ENTITY) T entity, @Param(Constants.WRAPPER) Wrapper 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 fieldList = tableInfo.getFieldList(); // 遍历 fieldList for (final TableFieldInfo tableFieldInfo : fieldList) { // 反射获取 TableFieldInfo 的 class 对象 final Class 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 getMethodList(Class mapperClass) { // 此 SQL 注入器继承了 DefaultSqlInjector (默认注入器),调用了 DefaultSqlInjector 的 getMethodList 方法,保留了 mybatis-plus 自带的方法 List 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() .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



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有