文章目录
- @[toc]
- 1.问题
- 2.原因
- 3.解决方法
- 3.1错误方法
- 方式一:配置全局字段策略
- 方式二:在实体上添加字段策略注解
- 3.2正确姿势
- 方式一:使用LambdaUpdateWrapper (推荐)
- 方式二:使用UpdateWrapper
- 方式三
- 总结
文章目录
- @[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<T, R>所以就可以传Lambda接口表达式的那种写法
AbstractWrapper类的关系图如下:
AbstractLambdaWrapper类的关系图如下:
SFunction<T, R>接口源码如下:
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 表达式中推测实体类
* <p>
* 如果获取不到列信息,那么本次条件组装将会失败
*
* @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 <T> 类型,被调用的 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 变更为大写格式
*
* <pre>
* Assert.assertEquals("USERID", formatKey("userId"))
* </pre>
*
* @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<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);
}
}
}
/**
* <p>
* 获取 注入的方法
* </p>
*
* @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的思想来实现的,有兴趣的小伙伴可以去看下源码,我的分享就到这里,希望能给你带来帮助,喜欢的话,请一键三连加关注,么么哒!