MyBatisPlus更新字段为null的正确姿势以及lambda方式的条件字段解析之源码解析

1.问题

      由于在项目中使用MyBatisPlus的updateById(Entity)接口api根据用户点击不同的操作切换,需要根据表里面的主键id更新表的字段为null的操作,在使用这个接口api根据主键设置实体字段为null更新居然不生效,也是奇奇怪怪的问题。


2.原因

     原因是MyBatisPlus的字段更新策略惹的祸,MyBatisPlus有以下几种策略:

     常用和主要的就前面三个

public enum FieldStrategy {
   IGNORED, //忽略
   NOT_NULL, //非NULL,默认策略,不忽略""
   NOT_EMPTY, //非空,会忽略"",会忽略NULL
   DEFAULT, // 默认
   NEVER;  //无

   private FieldStrategy() {
  }
}

       由于默认策略是NOT_NULL,就会导致以下两个api更新实体字段为null到表中失效,该策略是只会更新实体中非null字段的值,为null的字段会被剔除忽略。

this.updateById(entity);
this.update(entity, updateWrapper);


3.解决方法

3.1错误方法

方式一:配置全局字段策略

mybatis-plus:
global-config:
#字段策略 0:"忽略判断",1:"非 NULL 判断",2:"非空判断"
  field-strategy: 0

      全局的这种没有配置调试过,只试了下面方式二的情况是有问题的,这种方法是全局的影响会有点广,所以要谨慎使用

方式二:在实体上添加字段策略注解

@TableField(updateStrategy = FieldStrategy.IGNORED)

      方式二在实体字段上加了改注解,调试发现会生效,运行一次后数据库表中的字段被设置位null了,然后在运行几次,修改的实体字段设置有值的情况去更新,发现不会设置值了不更新,表中字段还是null,这个也是一个大坑,所以姿势不对就会很坑。


3.2正确姿势

方式一:使用LambdaUpdateWrapper (推荐)

LambdaUpdateWrapper<WhiteListManagementEntity> 
   lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
//条件
lambdaUpdateWrapper.eq(WhiteListManagementEntity::getId,
                      updateWhiteListManagementEntity.getId());
//设置字段值
lambdaUpdateWrapper.set(WhiteListManagementEntity::getIsNight, 0);
lambdaUpdateWrapper.set(WhiteListManagementEntity::getNightStart, null);
lambdaUpdateWrapper.set(WhiteListManagementEntity::getNightEnd, null);
//更新
this.update(lambdaUpdateWrapper);

     这种方式简洁也巧妙,巧妙的点是用到了JDK8的Lambda语法特性,使用了SerializedLambda来解析实体字段然后拼接为查询条件

      这个抽象AbstractWrapper的AbstractLambdaWrapper子类泛型的参数SFunction所以就可以传Lambda接口表达式的那种写法

      AbstractWrapper类的关系图如下:

       AbstractLambdaWrapper类的关系图如下:

      SFunction接口源码如下:

package com.baomidou.mybatisplus.core.toolkit.support;
import java.io.Serializable;import java.util.function.Function;
/** * 支持序列化的 Function * * @author miemie * @since 2018-05-12 */@FunctionalInterfacepublic interface SFunction extends Function, Serializable {}

       LambdaUpdateWrapper的set方法源码如下:

 @Override  public LambdaUpdateWrapper set(boolean condition, SFunction column, Object val) {      if (condition) {         sqlSet.add(String.format("%s=%s", columnToString(column), formatSql("{0}", val)));      }      return typedThis;  } @Override    protected String columnToString(SFunction column) {        return columnToString(column, true);    }
protected String columnToString(SFunction column, boolean onlyColumn) { return getColumn(LambdaUtils.resolve(column), onlyColumn); }
/** * 获取 SerializedLambda 对应的列信息,从 lambda 表达式中推测实体类 *

* 如果获取不到列信息,那么本次条件组装将会失败 * * @param lambda lambda 表达式 * @param onlyColumn 如果是,结果: "name", 如果否:"name" as "name" * @return * @throws com.baomidou.mybatisplus.core.exceptions.MybatisPlusException 获取不到列信息时抛出异常 * @see SerializedLambda#getImplClass() * @see SerializedLambda#getImplMethodName() */ private String getColumn(SerializedLambda lambda, boolean onlyColumn) throws MybatisPlusException { //使用反射解析SerializedLambda对象的is/get开头的方法拿到属性字段的名称 String fieldName = PropertyNamer.methodToProperty(lambda.getImplMethodName()); Class aClass = lambda.getInstantiatedType(); if (!initColumnMap) { columnMap = LambdaUtils.getColumnMap(aClass); initColumnMap = true; } Assert.notNull(columnMap, "can not find lambda cache for this entity [%s]", aClass.getName()); ColumnCache columnCache = columnMap.get(LambdaUtils.formatKey(fieldName)); Assert.notNull(columnCache, "can not find lambda cache for this property [%s] of entity [%s]", fieldName, aClass.getName()); return onlyColumn ? columnCache.getColumn() : columnCache.getColumnSelect(); }

        PropertyNamer的methodToProperty方法源码如下:

public static String methodToProperty(String name) {        if (name.startsWith("is")) {            name = name.substring(2);        } else {            if (!name.startsWith("get") && !name.startsWith("set")) {                throw new ReflectionException("Error parsing property name '" + name + "'.  Didn't start with 'is', 'get' or 'set'.");            }
name = name.substring(3); }
if (name.length() == 1 || name.length() > 1 && !Character.isUpperCase(name.charAt(1))) { name = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1); }
return name; }

   Lambda表达式的解析是通过这个LambdaUtils工具类的LambdaUtils.resolve(column)方法实现的,源码如下:

package com.baomidou.mybatisplus.core.toolkit;
import com.baomidou.mybatisplus.core.metadata.TableInfo;import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;import com.baomidou.mybatisplus.core.toolkit.support.ColumnCache;import com.baomidou.mybatisplus.core.toolkit.support.SFunction;import com.baomidou.mybatisplus.core.toolkit.support.SerializedLambda;
import java.lang.ref.WeakReference;import java.util.HashMap;import java.util.Map;import java.util.Optional;import java.util.concurrent.ConcurrentHashMap;
import static java.util.Locale.ENGLISH;
/** * Lambda 解析工具类 * * @author HCL, MieMie * @since 2018-05-10 */public final class LambdaUtils {
/** * 字段映射 */ private static final Map<String, Map<String, ColumnCache>> COLUMN_CACHE_MAP = new ConcurrentHashMap<>();
/** * SerializedLambda 反序列化缓存 */ private static final Map<String, WeakReference<SerializedLambda>> FUNC_CACHE = new ConcurrentHashMap<>();
/** * 解析 lambda 表达式, 该方法只是调用了 {@link SerializedLambda#resolve(SFunction)} 中的方法,在此基础上加了缓存。 * 该缓存可能会在任意不定的时间被清除 * * @param func 需要解析的 lambda 对象 * @param 类型,被调用的 Function 对象的目标类型 * @return 返回解析后的结果 * @see SerializedLambda#resolve(SFunction) */ public static <T> SerializedLambda resolve(SFunction<T, ?> func) { Class clazz = func.getClass(); String canonicalName = clazz.getCanonicalName(); return Optional.ofNullable(FUNC_CACHE.get(canonicalName)) .map(WeakReference::get) .orElseGet(() -> { SerializedLambda lambda = SerializedLambda.resolve(func); FUNC_CACHE.put(canonicalName, new WeakReference<>(lambda)); return lambda; }); }
/** * 格式化 key 将传入的 key 变更为大写格式 * *
     *     Assert.assertEquals("USERID", formatKey("userId"))     * 
* * @param key key * @return 大写的 key */ public static String formatKey(String key) { return key.toUpperCase(ENGLISH); }
/** * 将传入的表信息加入缓存 * * @param tableInfo 表信息 */ public static void installCache(TableInfo tableInfo) { COLUMN_CACHE_MAP.put(tableInfo.getEntityType().getName(), createColumnCacheMap(tableInfo)); }
/** * 缓存实体字段 MAP 信息 * * @param info 表信息 * @return 缓存 map */ private static Map<String, ColumnCache> createColumnCacheMap(TableInfo info) { Map<String, ColumnCache> map = new HashMap<>();
String kp = info.getKeyProperty(); if (StringUtils.isNotBlank(kp)) { map.put(formatKey(kp), new ColumnCache(info.getKeyColumn(), info.getKeySqlSelect())); }
info.getFieldList().forEach(i -> map.put(formatKey(i.getProperty()), new ColumnCache(i.getColumn(), i.getSqlSelect())) ); return map; }
/** * 获取实体对应字段 MAP * * @param clazz 实体类 * @return 缓存 map */ public static Map<String, ColumnCache> getColumnMap(Class clazz) { return COLUMN_CACHE_MAP.computeIfAbsent(clazz.getName(), key -> { TableInfo info = TableInfoHelper.getTableInfo(clazz); return info == null ? null : createColumnCacheMap(info); }); } }

      通过LambdaUtils.resolve(column)方法的解析可以获取到SerializedLambda的解析对象,只不多SerializedLambda类是MyBatisPlus的作者从JDK8源码中拷贝到项目中的,SerializedLambda位置如下:

         解析Lambda表达式就是调用该类的resolve方法:

SerializedLambda lambda = SerializedLambda.*resolve*(func);

        LambdaUtils中的installCache初始化TableInfo加入缓存逻辑入口是加了@TableName实体解析和条件匹配通过SerializedLambda连接起来的桥梁,至于TableInfo的解析初始化流程使用idea反向顺藤摸瓜去找这个方法的上层,一层一层的找就知道真正的入口在哪里了,正在的入口是在mybatis构建Session的时候

解析MyBatisPlus的映射,有XML类型的和Mapper接口注解类型的映射解析,MybatisSqlSessionFactoryBuilder.build()方法:

package com.baomidou.mybatisplus.core;
import com.baomidou.mybatisplus.core.config.GlobalConfig;import com.baomidou.mybatisplus.core.incrementer.DefaultIdentifierGenerator;import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;import com.baomidou.mybatisplus.core.injector.SqlRunnerInjector;import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;import com.baomidou.mybatisplus.core.toolkit.IdWorker;import org.apache.ibatis.exceptions.ExceptionFactory;import org.apache.ibatis.executor.ErrorContext;import org.apache.ibatis.session.Configuration;import org.apache.ibatis.session.SqlSessionFactory;import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import java.io.IOException;import java.io.InputStream;import java.io.Reader;import java.util.Properties;
/** * 重写SqlSessionFactoryBuilder * * @author nieqiurong 2019/2/23. */public class MybatisSqlSessionFactoryBuilder extends SqlSessionFactoryBuilder {
@SuppressWarnings("Duplicates") @Override public SqlSessionFactory build(Reader reader, String environment, Properties properties) { try { //TODO 这里换成 MybatisXMLConfigBuilder 而不是 XMLConfigBuilder MybatisXMLConfigBuilder parser = new MybatisXMLConfigBuilder(reader, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { reader.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } }
@SuppressWarnings("Duplicates") @Override public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { try { //TODO 这里换成 MybatisXMLConfigBuilder 而不是 XMLConfigBuilder MybatisXMLConfigBuilder parser = new MybatisXMLConfigBuilder(inputStream, environment, properties); return build(parser.parse()); } catch (Exception e) { throw ExceptionFactory.wrapException("Error building SqlSession.", e); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException e) { // Intentionally ignore. Prefer previous error. } } }
// TODO 使用自己的逻辑,注入必须组件 @Override public SqlSessionFactory build(Configuration config) { MybatisConfiguration configuration = (MybatisConfiguration) config; GlobalConfig globalConfig = GlobalConfigUtils.getGlobalConfig(configuration); final IdentifierGenerator identifierGenerator; if (globalConfig.getIdentifierGenerator() == null) { if (null != globalConfig.getWorkerId() && null != globalConfig.getDatacenterId()) { identifierGenerator = new DefaultIdentifierGenerator(globalConfig.getWorkerId(), globalConfig.getDatacenterId()); } else { identifierGenerator = new DefaultIdentifierGenerator(); } globalConfig.setIdentifierGenerator(identifierGenerator); } else { identifierGenerator = globalConfig.getIdentifierGenerator(); } //TODO 这里只是为了兼容下,并没多大重要,方法标记过时了. IdWorker.setIdentifierGenerator(identifierGenerator);
if (globalConfig.isEnableSqlRunner()) { new SqlRunnerInjector().inject(configuration); }
SqlSessionFactory sqlSessionFactory = super.build(configuration);
// 缓存 sqlSessionFactory globalConfig.setSqlSessionFactory(sqlSessionFactory);
return sqlSessionFactory; }}

       具体走那个根据解析参数类型来,根据mybatis的调用,总之会有一个方法被调用到的,只不过mybaisPlus对这个SqlSession的构建逻辑进行了重写和扩展,总体上是在mybatis的流程和实现上做了增强和简化,简化操作和提高了效率这是被开发者喜欢和信赖最有价值的地方

       最后会在MybatisMapperAnnotationBuilder类中的Mapper接口注册解析parse()接口里面有一段代码逻辑:

@Override    public void parse() {        String resource = type.toString();        if (!configuration.isResourceLoaded(resource)) {             ,,,,,,,,,,,            // TODO 注入 CURD 动态 SQL , 放在在最后, because 可能会有人会用注解重写sql            if (GlobalConfigUtils.isSupperMapperChildren(configuration, type)) {                GlobalConfigUtils.getSqlInjector(configuration).inspectInject(assistant, type);            }        }        parsePendingMethods();    }

      然后走到抽象sql注入器AbstractSqlInjector的inspectInject()方法中:

package com.baomidou.mybatisplus.core.injector;
import com.baomidou.mybatisplus.core.metadata.TableInfo;import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;import com.baomidou.mybatisplus.core.toolkit.ArrayUtils;import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;import com.baomidou.mybatisplus.core.toolkit.GlobalConfigUtils;import org.apache.ibatis.builder.MapperBuilderAssistant;import org.apache.ibatis.logging.Log;import org.apache.ibatis.logging.LogFactory;
import java.lang.reflect.ParameterizedType;import java.lang.reflect.Type;import java.lang.reflect.TypeVariable;import java.lang.reflect.WildcardType;import java.util.List;import java.util.Set;
/** * SQL 自动注入器 * * @author hubin * @since 2018-04-07 */public abstract class AbstractSqlInjector implements ISqlInjector {
private static final Log logger = LogFactory.getLog(AbstractSqlInjector.class);
@Override public void inspectInject(MapperBuilderAssistant builderAssistant, Class mapperClass) { Class modelClass = extractModelClass(mapperClass); if (modelClass != null) { String className = mapperClass.toString(); Set mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration()); if (!mapperRegistryCache.contains(className)) { List methodList = this.getMethodList(mapperClass); if (CollectionUtils.isNotEmpty(methodList)) { //这里就是初始tableInfo信息加入到缓存Map中的逻辑 TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass); // 循环注入自定义方法 methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo)); } else { logger.debug(mapperClass.toString() + ", No effective injection method was found."); } mapperRegistryCache.add(className); } } }
/** *

* 获取 注入的方法 *

* * @param mapperClass 当前mapper * @return 注入的方法集合 * @since 3.1.2 add mapperClass */ public abstract List getMethodList(Class mapperClass);
/** * 提取泛型模型,多泛型的时候请将泛型T放在第一位 * * @param mapperClass mapper 接口 * @return mapper 泛型 */ protected Class extractModelClass(Class mapperClass) { Type[] types = mapperClass.getGenericInterfaces(); ParameterizedType target = null; for (Type type : types) { if (type instanceof ParameterizedType) { Type[] typeArray = ((ParameterizedType) type).getActualTypeArguments(); if (ArrayUtils.isNotEmpty(typeArray)) { for (Type t : typeArray) { if (t instanceof TypeVariable || t instanceof WildcardType) { break; } else { target = (ParameterizedType) type; break; } } } break; } } return target == null ? null : (Class) target.getActualTypeArguments()[0]; }}

方式二:使用UpdateWrapper

     UpdateWrapper和LambdaUpdateWrapper 的区别是写法不一样,第一个参数需要写表的字段名(xxx_xxx)

UpdateWrapper       updateWrapper = new UpdateWrapper();//条件updateWrapper.eq("id", updateWhiteListManagementEntity.getId());  //设置字段值updateWrapper.set("is_night", 0);updateWrapper.set("night_start", null);updateWrapper.set("night_end", null);//更新this.update(updateWrapper);

方式三

在实体上加上如下注解

@TableField(fill = FieldFill.UPDATE)
//然后在调用如下方法
this.updateById(entity);
this.update(entity, updateWrapper);

      这种方式没有亲测过,只有方式一和方式二亲测有效的,方式一和方式二我在实体上都加了这个注解了也是有效的。


总结

      很多持久层ORM框架都是使用了JDK8的Lambda表达式的特性和这个类SerializedLambda解析Lambda表达式接口对象信息,然后使用Java的反射或者是字节码技术对Clazz文件做进一步处理和解析,比如:EasyEs开源框架,使用ORM的思想让操作ES数据库变得简单和高效 ,这款框架的ORM层的解析思想也是借鉴了MyBatisPlus的思想来实现的,有兴趣的小伙伴可以去看下源码,我的分享就到这里,希望能给你带来帮助,喜欢的话,请一键三连加关注,么么哒!


请使用浏览器的分享功能分享到微信等