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

文章目录

    • @[toc]
  • 1.问题
  • 2.原因
  • 3.解决方法
    • 3.1错误方法
      • 方式一:配置全局字段策略
      • 方式二:在实体上添加字段策略注解
    • 3.2正确姿势
      • 方式一:使用LambdaUpdateWrapper (推荐)
      • 方式二:使用UpdateWrapper
      • 方式三
  • 总结

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类的关系图如下:

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

  AbstractLambdaWrapper类的关系图如下:

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

  SFunction接口源码如下:

package com.baomidou.mybatisplus.core.toolkit.support;

import java.io.Serializable;
import java.util.function.Function;

/**
 * 支持序列化的 Function
 *
 * @author miemie
 * @since 2018-05-12
 */
@FunctionalInterface
public interface SFunction<T, R> extends Function<T, R>, Serializable {
}

  LambdaUpdateWrapper的set方法源码如下:

 @Override
  public LambdaUpdateWrapper<T> set(boolean condition, SFunction<T, ?> column, Object val) {
      if (condition) {
         sqlSet.add(String.format("%s=%s", columnToString(column), formatSql("{0}", val)));
      }
      return typedThis;
  }
 @Override
    protected String columnToString(SFunction<T, ?> column) {
        return columnToString(column, true);
    }

    protected String columnToString(SFunction<T, ?> 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位置如下:

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

  解析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<String> mapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration());
            if (!mapperRegistryCache.contains(className)) {
                List<AbstractMethod> 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<AbstractMethod> 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<WhiteListManagementEntity> 
      updateWrapper = new UpdateWrapper<WhiteListManagementEntity>();
//条件
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的思想来实现的,有兴趣的小伙伴可以去看下源码,我的分享就到这里,希望能给你带来帮助,喜欢的话,请一键三连加关注,么么哒!

你可能感兴趣的:(java,mybatis,MyBatisPlus)