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
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 SFunctionextends Function , Serializable { }
LambdaUpdateWrapper的set方法源码如下:
public LambdaUpdateWrapperset(boolean condition, SFunction {column, Object val) if (condition) {sqlSet.add(String.format("%s=%s", columnToString(column), formatSql("{0}", val)));}return typedThis;}protected String columnToString(SFunctioncolumn) {return columnToString(column, true);}protected String columnToString(SFunctioncolumn, 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 {("Duplicates")public SqlSessionFactory build(Reader reader, String environment, Properties properties) {try {//TODO 这里换成 MybatisXMLConfigBuilder 而不是 XMLConfigBuilderMybatisXMLConfigBuilder 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.}}}("Duplicates")public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) {try {//TODO 这里换成 MybatisXMLConfigBuilder 而不是 XMLConfigBuilderMybatisXMLConfigBuilder 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 使用自己的逻辑,注入必须组件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);// 缓存 sqlSessionFactoryglobalConfig.setSqlSessionFactory(sqlSessionFactory);return sqlSessionFactory;}}
具体走那个根据解析参数类型来,根据mybatis的调用,总之会有一个方法被调用到的,只不过mybaisPlus对这个SqlSession的构建逻辑进行了重写和扩展,总体上是在mybatis的流程和实现上做了增强和简化,简化操作和提高了效率这是被开发者喜欢和信赖最有价值的地方
最后会在MybatisMapperAnnotationBuilder类中的Mapper接口注册解析parse()接口里面有一段代码逻辑:
public void parse() {String resource = type.toString();if (!configuration.isResourceLoaded(resource)) {,,,,,,,,,,,// TODO 注入 CURD 动态 SQL , 放在在最后, because 可能会有人会用注解重写sqlif (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);public void inspectInject(MapperBuilderAssistant builderAssistant, Class> mapperClass) {Class> modelClass = extractModelClass(mapperClass);if (modelClass != null) {String className = mapperClass.toString();SetmapperRegistryCache = GlobalConfigUtils.getMapperRegistryCache(builderAssistant.getConfiguration()); if (!mapperRegistryCache.contains(className)) {ListmethodList = 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 ListgetMethodList(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)
UpdateWrapperupdateWrapper = 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的思想来实现的,有兴趣的小伙伴可以去看下源码,我的分享就到这里,希望能给你带来帮助,喜欢的话,请一键三连加关注,么么哒!