mybatis-plus中更新null值的問題解決
前言
本文主要介紹 mybatis-plus 中常使用的 update 相關方法的區(qū)別,以及更新 null 的方法有哪些等。
至于為什么要寫這篇文章,首先是在開發(fā)中確實有被坑過幾次,導致某些字段設置為 null 值設置不上,其次是官方文檔對于這塊內容并沒有提供一個很完善的解決方案,所以我就總結一下。
一、情景介紹
關于 Mybatis-plus 這里我就不多做介紹了,如果之前沒有使用過該項技術的可參考以下鏈接進行了解。
mybatis-plus 官方文檔:https://baomidou.com/

我們在使用 mybatis-plus 進行開發(fā)時,默認情況下, mybatis-plus 在更新數(shù)據(jù)時時會判斷字段是否為 null,如果是 null 則不設置值,也就是更新后的該字段數(shù)據(jù)依然是原數(shù)據(jù),雖然說這種方式在一定程度上可以避免數(shù)據(jù)缺失等問題,但是在某些業(yè)務場景下我們就需要設置某些字段的數(shù)據(jù)為 null。
二、方法分析
這里我準備了一個 student 表進行測試分析,該表中僅有兩條數(shù)據(jù):
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() 的方法更新數(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 不設置屬性
恢復 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 的字段會直接跳過設置,執(zhí)行 sql 與上面一樣:
UPDATE student SET name = '李大霄' WHERE id = 1
update() 方法 — UpdateWrapper 設置屬性
恢復 student 表中的數(shù)據(jù)為初始數(shù)據(jù)。
因為 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
查看數(shù)據(jù)庫:
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() 代碼

從控制臺打印的日志可以看出,均執(zhí)行 sql:
UPDATE student SET name='李大霄', age=null WHERE id = 1
所以可知將字段更新策略設置為: FieldStrategy.IGNORED 就能更新數(shù)據(jù)庫的數(shù)據(jù)為 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 的方式,也是是說不管設置了什么值都會是 字段=值 的形式,所以就會被設置上去。
四、解決方式
從上文分析就可以知道已經有兩種方式實現(xiàn)更新 null ,不過除此之外就是直接修改全局配置,所以這三種方法分別是:
方式一:修改單個字段策略模式
這種方式在上文已經敘述過了,直接在實體類上指定其修改策略模式即可
@TableField(updateStrategy = FieldStrategy.IGNORED)

如果某些字段需要可以在任何時候都能更新為 null,這種方式可以說是最方便的了。
方式二:修改全局策略模式
通過剛剛分析源碼可知,如果沒有指定字段的策略,取的是 DbConfig 中的配置,而 DbConfig 是 GlobalConfig 的靜態(tài)內部類

所以我們可以通過修改全局配置的方式,改變 updateStrategy 的策略不就行了嗎?
yml 方式配置如下
mybatis-plus:
global-config:
db-config:
update-strategy: IGNORED
注釋 @TableField(updateStrategy = FieldStrategy.IGNORED)

恢復 student 表中的數(shù)據(jù)為初始數(shù)據(jù),進行測試。

可以看到是可行的,執(zhí)行的 sql 為:
UPDATE student SET name='李大霄', age=null WHERE id = 1
但是值得注意的是,這種全局配置的方法會對所有的字段都忽略判斷,如果一些字段不想要修改,也會因為傳的是 null 而修改,導致業(yè)務數(shù)據(jù)的缺失,所以并不推薦使用。
方式三:使用 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 值,但是如果在某個表中,某個業(yè)務場景下需要全量更新 null 值,而且這個表的字段又很多,一個個 set 真的很折磨人,像 tk.mapper 都有方法進行全量更新 null 值,那有沒有什么方法可以全量更新?
雖然 mybaatis-plus 沒有,但是可以自己去實現(xiàn),我是看了起風哥:讓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() 方法
/**
* 駝峰命名轉下劃線
* @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ī)擴展—實現(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);
}
構造一個方法 UpdateWithNull 的方法類
public class UpdateWithNull extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// 處理邏輯
return null;
}
}
之前說過可以設置字段的更新策略屬性為:FieldStrategy.IGNORED 使其可以更新 null 值,現(xiàn)在方法參數(shù)中有 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 表中的數(shù)據(jù)為初始數(shù)據(jù),進行測試。
測試代碼:
@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 ,這種方式我是覺得比較推薦的,增加了系統(tǒng)的擴展性和靈活性。
擴展
MybatisPlus update 更新時指定要更新為 null 的方法
讓mybatis-plus支持null字段全量更新
Mybatis-Plus中update()和updateById()將字段更新為null
Mybatis-Plus中update更新操作用法
MyBatis-plus源碼解析
到此這篇關于mybatis-plus中更新null值的問題解決的文章就介紹到這了,更多相關mybatis-plus 更新null值內容請搜索腳本之家以前的文章或繼續(xù)瀏覽
相關文章
freemarker簡介_動力節(jié)點Java學院整理
FreeMarker是一個模板引擎,一個基于模板生成文本輸出的通用工具,使用純Java編寫,有興趣的可以了解一下2017-08-08
Java中超過1000萬數(shù)據(jù)就自動轉為科學計數(shù)法的原因分析與解決方案
在 Java 開發(fā)中,你是否遇到過這樣的場景:當數(shù)值達到 1000 萬(10^7)時,原本期望的整數(shù)格式突然變成了1.0E7這樣的科學計數(shù)法?本文將深入分析背后的原因,并提供 3 種實用解決方案,幫你徹底解決這個問題,需要的朋友可以參考下2026-03-03

