mybatis-plus中更新null值的問題解決
前言
本文主要介紹 mybatis-plus 中常使用的 update 相關(guān)方法的區(qū)別,以及更新 null 的方法有哪些等。
至于為什么要寫這篇文章,首先是在開發(fā)中確實有被坑過幾次,導(dǎo)致某些字段設(shè)置為 null 值設(shè)置不上,其次是官方文檔對于這塊內(nèi)容并沒有提供一個很完善的解決方案,所以我就總結(jié)一下。
一、情景介紹
關(guān)于 Mybatis-plus 這里我就不多做介紹了,如果之前沒有使用過該項技術(shù)的可參考以下鏈接進(jìn)行了解。
mybatis-plus 官方文檔:https://baomidou.com/
我們在使用 mybatis-plus 進(jìn)行開發(fā)時,默認(rèn)情況下, mybatis-plus 在更新數(shù)據(jù)時時會判斷字段是否為 null,如果是 null 則不設(shè)置值,也就是更新后的該字段數(shù)據(jù)依然是原數(shù)據(jù),雖然說這種方式在一定程度上可以避免數(shù)據(jù)缺失等問題,但是在某些業(yè)務(wù)場景下我們就需要設(shè)置某些字段的數(shù)據(jù)為 null。
二、方法分析
這里我準(zhǔn)備了一個 student
表進(jìn)行測試分析,該表中僅有兩條數(shù)據(jù):
mysql> SELECT * FROM student; +-----+---------+----------+ | id | name | age | +-----+---------+----------+ | 1 | 米大傻 | 18 | +-----+---------+----------+ | 2 | 米大哈 | 20 | +-----+---------+----------+
在 mybatis-plus 中,我們的 mapper 類都會繼承 BaseMapper
這樣一個類
public interface StudentMapper extends BaseMapper<Student> { }
進(jìn)入到 BaseMapper
這個接口可以查看到該類僅有兩個方法和更新有關(guān)(這里我就不去分析 IService
類中的那些更新方法了,因為那些方法低層最后也是調(diào)用了 BaseMapper
中的這兩個 update 方法)
所以就從這兩個方法入手分析:
updateById() 方法
@Test public void testUpdateById() { Student student = studentMapper.selectById(1); student.setName("李大霄"); student.setAge(null); studentMapper.updateById(student); }
可以看到使用 updateById() 的方法更新數(shù)據(jù),盡管在代碼中將 age 賦值為 null
,但是最后執(zhí)行的 sql 確是:
UPDATE student SET name = '李大霄' WHERE id = 1
也就是說在數(shù)據(jù)庫中,該條數(shù)據(jù)的 name
值發(fā)生了變化,但是 age
保持不變
mysql> SELECT * FROM student WHERE id = 1; +-----+---------+----------+ | id | name | age | +-----+---------+----------+ | 1 | 李大霄 | 18 | +-----+---------+----------+
update() 方法 — UpdateWrapper 不設(shè)置屬性
恢復(fù) student
表中的數(shù)據(jù)為初始數(shù)據(jù)。
@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
的字段會直接跳過設(shè)置,執(zhí)行 sql 與上面一樣:
UPDATE student SET name = '李大霄' WHERE id = 1
update() 方法 — UpdateWrapper 設(shè)置屬性
恢復(fù) student
表中的數(shù)據(jù)為初始數(shù)據(jù)。
因為 UpdateWrapper
是可以去字段屬性的,所以再測試下 UpdateWrapper
中設(shè)置為 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()) ); }
從打印的日志信息來看,是可以設(shè)置 null
值的,sql 為:
UPDATE student SET name='李大霄', age=null WHERE id = 1
查看數(shù)據(jù)庫:
mysql> SELECT * FROM student WHERE id = 1; +-----+---------+----------+ | id | name | age | +-----+---------+----------+ | 1 | 李大霄 | NULL | +-----+---------+----------+
三、原因分析
從方法分析中我們可以得出,如果不使用 UpdateWrapper
進(jìn)行設(shè)置值,通過 BaseMapper
的更新方法是沒法設(shè)置為 null
的,可以猜出 mybatis-plus 在默認(rèn)的情況下就會跳過屬性為 null
值的字段,不進(jìn)行設(shè)值。
通過查看官方文檔可以看到, mybatis-plus 有幾種字段策略:
也就是說在默認(rèn)情況下,字段策略應(yīng)該是 FieldStrategy.NOT_NULL
跳過 null
值的
可以先設(shè)置實體類的字段更新策略為 FieldStrategy.IGNORED
來驗證是否會忽略判斷 null
@Data @EqualsAndHashCode(callSuper = true) @ApiModel(value="Student對象", description="學(xué)生表") 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) // 設(shè)置字段策略為:忽略判斷 private String name; @ApiModelProperty(value = "年齡") @TableField(updateStrategy = FieldStrategy.IGNORED) // 設(shè)置字段策略為:忽略判斷 private Integer age; }
再運行以上 testUpdateById()
和 testUpdate()
代碼
從控制臺打印的日志可以看出,均執(zhí)行 sql:
UPDATE student SET name='李大霄', age=null WHERE id = 1
所以可知將字段更新策略設(shè)置為: FieldStrategy.IGNORED
就能更新數(shù)據(jù)庫的數(shù)據(jù)為 null
了
翻閱 @TableField
注解的源碼:
可以看到在源碼中,如果沒有進(jìn)行策略設(shè)置的話,它默認(rèn)的策略就是 FieldStrategy.DEFAULT
的,那為什么最后處理的結(jié)果是使用了 NOT_NULL
的策略呢?
再追進(jìn)源碼中,可以得知每個實體類都對應(yīng)一個 TableInfo
對象,而實體類中每一個屬性都對應(yīng)一個 TableFieldInfo
對象
進(jìn)入到 TableFieldInfo
類中查看該類的屬性是有 updateStrategy(修改屬性策略的)
查看構(gòu)造方法 TableFieldInfo()
可以看到如果字段策略為 FieldStrategy.DEFAULT
,取的是 dbConfig.getUpdateStrategy()
,如果字段策略不等于 FieldStrategy.DEFAULT
,則取注解類 TableField
指定的策略類型。
點擊進(jìn)入對象 dbConfig
所對應(yīng)的類 DbConfig
中
可以看到在這里 DbConfig 默認(rèn)的 updateStrategy
就是 FieldStrategy.NOT_NULL
,所以說 mybatis-plus
默認(rèn)情況下就是跳過 null
值不設(shè)置的。
那為什么通過 UpdateWrapper
的 set
方法就可以設(shè)置值呢?
同樣取查看 set()
方法的源碼:
看到這行代碼已經(jīng)明了,因為可以看到它是通過 String.format("%s=%s",字段,值)
拼接 sql 的方式,也是是說不管設(shè)置了什么值都會是 字段=值
的形式,所以就會被設(shè)置上去。
四、解決方式
從上文分析就可以知道已經(jīng)有兩種方式實現(xiàn)更新 null
,不過除此之外就是直接修改全局配置,所以這三種方法分別是:
方式一:修改單個字段策略模式
這種方式在上文已經(jīng)敘述過了,直接在實體類上指定其修改策略模式即可
@TableField(updateStrategy = FieldStrategy.IGNORED)
如果某些字段需要可以在任何時候都能更新為 null
,這種方式可以說是最方便的了。
方式二:修改全局策略模式
通過剛剛分析源碼可知,如果沒有指定字段的策略,取的是 DbConfig
中的配置,而 DbConfig
是 GlobalConfig
的靜態(tài)內(nèi)部類
所以我們可以通過修改全局配置的方式,改變 updateStrategy
的策略不就行了嗎?
yml
方式配置如下
mybatis-plus: global-config: db-config: update-strategy: IGNORED
注釋 @TableField(updateStrategy = FieldStrategy.IGNORED)
恢復(fù) student
表中的數(shù)據(jù)為初始數(shù)據(jù),進(jìn)行測試。
可以看到是可行的,執(zhí)行的 sql 為:
UPDATE student SET name='李大霄', age=null WHERE id = 1
但是值得注意的是,這種全局配置的方法會對所有的字段都忽略判斷,如果一些字段不想要修改,也會因為傳的是 null 而修改,導(dǎo)致業(yè)務(wù)數(shù)據(jù)的缺失,所以并不推薦使用。
方式三:使用 UpdateWrapper 進(jìn)行設(shè)置
這種方式前面也提到過了,就是使用 UpdateWrapper
或其子類進(jìn)行 set
設(shè)置,例如:
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
的操作,所以就不列其上。
五、方式擴(kuò)展
雖然上面提供了一些方法來更新 null 值,但是不得不說,各有弊端,雖然說是比較推薦使用 UpdateWrapper
來更新 null 值,但是如果在某個表中,某個業(yè)務(wù)場景下需要全量更新 null 值,而且這個表的字段又很多,一個個 set
真的很折磨人,像 tk.mapper
都有方法進(jìn)行全量更新 null 值,那有沒有什么方法可以全量更新?
雖然 mybaatis-plus
沒有,但是可以自己去實現(xiàn),我是看了起風(fēng)哥:讓mybatis-plus支持null字段全量更新 這篇博客,覺得蠻好的,所以整理下作此分享。
實現(xiàn)方式一:使用 UpdateWrapper
循環(huán)拼接 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()
方法
/** * 駝峰命名轉(zhuǎn)下劃線 * @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()); }
實現(xiàn)方式二:mybatis-plus常規(guī)擴(kuò)展—實現(xiàn) IsqlInjector
像 mybatis-plus 中提供的批量添加數(shù)據(jù)的 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); }
構(gòu)造一個方法 UpdateWithNull
的方法類
public class UpdateWithNull extends AbstractMethod { @Override public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) { // 處理邏輯 return null; } }
之前說過可以設(shè)置字段的更新策略屬性為:FieldStrategy.IGNORED
使其可以更新 null 值,現(xiàn)在方法參數(shù)中有 TableInfo
對象,通過 TableInfo
我們可以拿到所有的 TableFieldInfo
,通過反射設(shè)置所有的 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 設(shè)置為 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 (默認(rèn)注入器),調(diào)用了 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(); } ... }
我寫的目錄結(jié)構(gòu)大概長這樣(僅供參考):
恢復(fù) student
表中的數(shù)據(jù)為初始數(shù)據(jù),進(jìn)行測試。
測試代碼:
@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。
總結(jié)
以上就是我對 mybatis-plus
更新 null
值問題做的探討,結(jié)合測試實例與源碼分析,算是解釋得比較明白了,尤其是最后擴(kuò)展的兩種方法自認(rèn)為是比較符合我的需求的,最后擴(kuò)展的那兩種方法都在實體類 Mapper 和 mybatis-plus 的 BaseMapper
中間多抽了一層 GaeaBaseMapper
,這種方式我是覺得比較推薦的,增加了系統(tǒng)的擴(kuò)展性和靈活性。
擴(kuò)展
MybatisPlus update 更新時指定要更新為 null 的方法
讓mybatis-plus支持null字段全量更新
Mybatis-Plus中update()和updateById()將字段更新為null
Mybatis-Plus中update更新操作用法
MyBatis-plus源碼解析
到此這篇關(guān)于mybatis-plus中更新null值的問題解決的文章就介紹到這了,更多相關(guān)mybatis-plus 更新null值內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽
相關(guān)文章
Elasticsearch?percolate?查詢示例詳解
這篇文章主要為大家介紹了Elasticsearch?percolate?查詢示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01springboot+vue實現(xiàn)登錄功能的最新方法整理
最近做項目時使用到了springboot+vue實現(xiàn)登錄功能的技術(shù),所以下面這篇文章主要給大家介紹了關(guān)于springboot+vue實現(xiàn)登錄功能的相關(guān)資料,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06SpringBoot Maven打包插件spring-boot-maven-plugin無法解析原因
spring-boot-maven-plugin是spring boot提供的maven打包插件,本文主要介紹了SpringBoot Maven打包插件spring-boot-maven-plugin無法解析原因,具有一定的參考價值,感興趣的可以了解一下2024-03-03Java使用synchronized實現(xiàn)互斥鎖功能示例
這篇文章主要介紹了Java使用synchronized實現(xiàn)互斥鎖功能,結(jié)合實例形式分析了Java使用synchronized互斥鎖功能簡單實現(xiàn)方法與操作技巧,需要的朋友可以參考下2020-05-05springboot?@Validated的概念及示例實戰(zhàn)
這篇文章主要介紹了springboot?@Validated的概念以及實戰(zhàn),使用?@Validated?注解,Spring?Boot?應(yīng)用可以有效地實現(xiàn)輸入驗證,提高數(shù)據(jù)的準(zhǔn)確性和應(yīng)用的安全性,本文結(jié)合實例給大家講解的非常詳細(xì),需要的朋友可以參考下2024-04-04使用Spring?Cloud?Stream處理Java消息流的操作流程
Spring?Cloud?Stream是一個用于構(gòu)建消息驅(qū)動微服務(wù)的框架,能夠與各種消息中間件集成,如RabbitMQ、Kafka等,今天我們來探討如何使用Spring?Cloud?Stream來處理Java消息流,需要的朋友可以參考下2024-08-08SpringCloud組件之Eureka Server詳細(xì)啟動過程及說明
這篇文章主要介紹了SpringCloud組件之Eureka Server詳細(xì)啟動過程及說明,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01