MybatisPlus扩展,按需求保存null字段,继承AbstractMethod

本文主要是对MybatisPlus的更新方法进行扩展,对set语句的非空校验进行自定义判断,提供了两个方法模板

/**
 * 根据主键更新字段,null也会更新
 * @param entity
 * @author zhangyong
 * @date 2025/3/29
 * @return int
 */
int updateIgnoreNullById(@Param("et") T entity);
/**
 * 根据条件更新实体,null也会更新
 * @author zhangyong
 * @date 2025/3/29
 * @param entity:
 * @param updateWrapper:
 * @return int
 */
int updateIgnoreNull(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);

MybatisPlus扩展接口需要四个步骤,第五步为使用

1、定义方法扩展

这部分必须实现injectMappedStatement接口,接口的内容可以参考com.baomidou.mybatisplus.core.injector.methods包和com.baomidou.mybatisplus.extension.injector.methods.AlwaysUpdateSomeColumnById接口的写法。

主要的拼接setsql的方法有两个,可以参考里面的实现来实现我们自己的逻辑,没必要自己造轮子。

a: AbstractMethod.filterTableFieldInfo

b.AbstractMethod.sqlSet

其中调用的TableFieldInfo.convertIf接口非常重要,是用来生成set字段外的if条件的,可以按需求调整。如:

<if test="accountId != null">
    ac.account_id = #{accountId},
</if>

代码中需要注意

 return this.addUpdateMappedStatement(mapperClass, modelClass, "updateIgnoreNull", sqlSource);的updateIgnoreNull需要和Mapper里的方法名一样(重要)

a:根据自定义Wrapper条件更新数据,int updateIgnoreNullById(@Param("et") T entity);

package com.hk.abpms.data.api.config.method;

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 com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;

import java.util.function.Predicate;

/**
 * 根据自定义Wrapper条件更新数据
 *
 * @author zhangyong
 * @Date 2025/3/28
 */
public class UpdateIgnoreNull extends AbstractMethod {
    private Predicate<TableFieldInfo> predicate;
    public UpdateIgnoreNull() {
    }
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        SqlMethod sqlMethod = SqlMethod.UPDATE;
        if(tableInfo.getTableName().equals("cfg_api")){
            System.out.println(tableInfo.getFieldList());
        }
        String sqlSet = this.filterTableFieldInfo(tableInfo.getFieldList(), this.getPredicate(), (i) -> {
            //以下代码来自AbstractMethod.sqlSet
            String newPrefix = "et.";
            String sqlSet2 = i.getColumn() + "=";
            if (StringUtils.isNotBlank(i.getUpdate())) {
                sqlSet2 = sqlSet2 + String.format(i.getUpdate(), i.getColumn());
            } else {
                sqlSet2 = sqlSet2 + SqlScriptUtils.safeParam(newPrefix + i.getEl());
            }
            sqlSet2 = sqlSet2 + ",";
            return i.isWithUpdateFill() ? sqlSet2 : this.convertIf2(i,sqlSet2, this.convertIfProperty2(newPrefix, i.getProperty()), i.getUpdateStrategy());
        }, "\n");
        sqlSet = SqlScriptUtils.convertSet(sqlSet);
        String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlSet, this.sqlWhereEntityWrapper(true, tableInfo), this.sqlComment());
        SqlSource sqlSource = this.languageDriver.createSqlSource(this.configuration, sql, modelClass);
        return this.addUpdateMappedStatement(mapperClass, modelClass, "updateIgnoreNull", sqlSource);
    }
    private String convertIf2(TableFieldInfo tableFieldInfo,final String sqlScript, final String property, final FieldStrategy fieldStrategy) {
        //判断Entity上的注解
        if (fieldStrategy == FieldStrategy.NEVER) {
            //字段标记为用不更新的 不需要set语句。@TableField(updateStrategy = FieldStrategy.NEVER)
            return null;
        } else if (!tableFieldInfo.isPrimitive() && fieldStrategy != FieldStrategy.IGNORED) {
            //如果字段没有FieldStrategy.IGNORED注解(建议不要加这个注解,加了注解所有涉及这个Entity的保存该字段为null都会更新)
            if(tableFieldInfo.isWithInsertFill()){
                //判断Entity字段是否配置了 @TableField(fill = FieldFill.INSERT)注解,理论上有这个注解的不需要修改,比如createBy等字段,所以此处set外拼接了if为空判断
                return fieldStrategy == FieldStrategy.NOT_EMPTY && tableFieldInfo.isCharSequence() ? SqlScriptUtils.convertIf(sqlScript, String.format("%s != null and %s != ''", property, property), false) : SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", property), false);
            }else{
                //不添加if为空判断,null也会更新
                return sqlScript;
            }
        } else {
            //不添加if为空判断,null也会更新
            return sqlScript;
        }
    }
    private String convertIfProperty2(String prefix, String property) {
        return StringUtils.isNotBlank(prefix) ? prefix.substring(0, prefix.length() - 1) + "['" + property + "']" : property;
    }
    private Predicate<TableFieldInfo> getPredicate() {
        Predicate<TableFieldInfo> noLogic = (t) -> {
            return !t.isLogicDelete();
        };
        return this.predicate != null ? noLogic.and(this.predicate) : noLogic;
    }
}

 B:根据ID更新数据int updateIgnoreNull(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);

package com.hk.abpms.data.api.config.method;

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 com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;

import java.util.Objects;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * 根据ID更新数据
 *
 * @author zhangyong
 * @Date 2025/3/28
 */
public class UpdateIgnoreNullById extends AbstractMethod {
    public UpdateIgnoreNullById() {
    }
    @Override
    public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
        SqlMethod sqlMethod = SqlMethod.UPDATE_BY_ID;
        if(tableInfo.getTableName().equals("cfg_api")){
            System.out.println(tableInfo.getFieldList());
        }
        String additional = this.optlockVersion(tableInfo) + tableInfo.getLogicDeleteSql(true, true);
        String sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), this.sqlSet2(tableInfo.isWithLogicDelete(), false, tableInfo, false, "et", "et."), tableInfo.getKeyColumn(), "et." + tableInfo.getKeyProperty(), additional);
        SqlSource sqlSource = this.languageDriver.createSqlSource(this.configuration, sql, modelClass);
        return this.addUpdateMappedStatement(mapperClass, modelClass, "updateIgnoreNullById", sqlSource);
    }
    public String getAllSqlSet2( TableInfo table,boolean ignoreLogicDelFiled, final String prefix) {
        String newPrefix = prefix == null ? "" : prefix;
        return (String)table.getFieldList().stream().filter((i) -> {
            if (!ignoreLogicDelFiled) {
                return true;
            } else {
                return !table.isWithLogicDelete() || !i.isLogicDelete();
            }
        }).map((i) -> {
            String sqlSet2 = i.getColumn() + "=";
            if (StringUtils.isNotBlank(i.getUpdate())) {
                sqlSet2 = sqlSet2 + String.format(i.getUpdate(), i.getColumn());
            } else {
                sqlSet2 = sqlSet2 + SqlScriptUtils.safeParam(newPrefix + i.getEl());
            }
            sqlSet2 = sqlSet2 + ",";
            return i.isWithUpdateFill() ? sqlSet2 : this.convertIf2(i,sqlSet2, this.convertIfProperty2(newPrefix, i.getProperty()), i.getUpdateStrategy());
        }).filter(Objects::nonNull).collect(Collectors.joining("\n"));
    }
    protected String sqlSet2(boolean logic, boolean ew, TableInfo table, boolean judgeAliasNull, final String alias, final String prefix) {
        String sqlScript = this.getAllSqlSet2(table,logic, prefix);
        if (judgeAliasNull) {
            sqlScript = SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", alias), true);
        }
        if (ew) {
            sqlScript = sqlScript + "\n";
            sqlScript = sqlScript + SqlScriptUtils.convertIf(SqlScriptUtils.unSafeParam("ew.sqlSet"), String.format("%s != null and %s != null", "ew", "ew.sqlSet"), false);
        }

        sqlScript = SqlScriptUtils.convertSet(sqlScript);
        return sqlScript;
    }
    private String convertIf2(TableFieldInfo tableFieldInfo,final String sqlScript, final String property, final FieldStrategy fieldStrategy) {
        if (fieldStrategy == FieldStrategy.NEVER) {
            return null;
        } else if (!tableFieldInfo.isPrimitive() && fieldStrategy != FieldStrategy.IGNORED) {
            if(tableFieldInfo.isWithInsertFill()){
                return fieldStrategy == FieldStrategy.NOT_EMPTY && tableFieldInfo.isCharSequence() ? SqlScriptUtils.convertIf(sqlScript, String.format("%s != null and %s != ''", property, property), false) : SqlScriptUtils.convertIf(sqlScript, String.format("%s != null", property), false);
            }else{
                return sqlScript;
            }
        } else {
            return sqlScript;
        }
    }
    private String convertIfProperty2(String prefix, String property) {
        return StringUtils.isNotBlank(prefix) ? prefix.substring(0, prefix.length() - 1) + "['" + property + "']" : property;
    }
}

2、将方法加入MybatisPlus进行管理

package com.hk.abpms.data.api.config;

import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector;
import com.baomidou.mybatisplus.extension.injector.methods.AlwaysUpdateSomeColumnById;
import com.hk.abpms.data.api.config.method.UpdateIgnoreNull;
import com.hk.abpms.data.api.config.method.UpdateIgnoreNullById;

import java.util.List;

/**
 * sql方法注入
 *
 * @author zhangyong
 * @Date 2025/3/27
 */
public class MybatisSqlInjector extends DefaultSqlInjector {
    @Override
    public List<AbstractMethod> getMethodList(Class<?> mapperClass) {
        List<AbstractMethod> methodList = super.getMethodList(mapperClass);
        // 添加自定义的更新方法,AlwaysUpdateSomeColumnById 会更新所有字段,包括空字段
        methodList.add(new UpdateIgnoreNullById());
        methodList.add(new UpdateIgnoreNull());
        return methodList;
    }
}

3、将Injector加入springBean管理

package com.hk.abpms.data.api.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * MybatisPlus的额外配置类
 *
 * @author zhangyong
 * @Date 2025/3/28
 */
@Configuration
public class MyBatisPlusConfig {

    @Bean
    public MybatisSqlInjector customSqlInjector() {
        return new MybatisSqlInjector();
    }
}

4、在BaseMapper中定义与第一步同名的方法

注意参数@Param("et")名字要和第一步的名称对应

package com.hk.abpms.data.api.config;

import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Param;

/**
 * 基础mapper
 *
 * @author zhangyong
 * @Date 2025/3/28
 */
public interface BaseCommonMapper<T> extends BaseMapper<T> {
    /**
     * 根据主键更新字段,null也会更新
     * @param entity
     * @author zhangyong
     * @date 2025/3/29
     * @return int
     */
    int updateIgnoreNullById(@Param("et") T entity);
    /**
     * 根据条件更新实体,null也会更新
     * @author zhangyong
     * @date 2025/3/29
     * @param entity:
     * @param updateWrapper:
     * @return int
     */
    int updateIgnoreNull(@Param("et") T entity, @Param("ew") Wrapper<T> updateWrapper);
}

5、业务Mapper继承公共的BaseCommonMapper后就可正常使用里面的方法

欢迎各位大佬留言。