问题描述
在上一篇我们讲过 MyBatis-Plus - 论 1 个实体类被 N 个DAO 类绑定,导致 MP 特性(逻辑删)失效的解决方案-CSDN博客
所以在这个基础上,我们可以很好定位到源码的分析位置。
但是今天这个问题就更奇怪了,已经确保 1 个实体类只被 1 个 DAO 类绑定,可还是『逻辑删』失效啊~
于是,又开始苦逼的 Debug Mybatis-Plus 源码……
原因分析
- 再分析源码前,我们先得出几个现象级的结论
- 因为我们采用了 Allinone 的架构,所以才发现的问题
- 我们单独启动 A 服务是不会逻辑删失效,但是再加入个 B 服务,用 standalone 模式启动就失效了
- 先上一张关键图,我们看到红框这个在一开始的时候是 false,后来经过一顿操作变成了 true,以至于逻辑删生效,所以我们只需要关注这个 logicDelete 的变化逻辑是我们重点观察的对象
源码分析
无可厚非,这个 TableInfoHelper 类还是我们分析的重点
/*
* Copyright (c) 2011-2020, baomidou (jobob@qq.com).
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.baomidou.mybatisplus.core.metadata;
import com.baomidou.mybatisplus.annotation.*;
import com.baomidou.mybatisplus.core.config.GlobalConfig;
import com.baomidou.mybatisplus.core.incrementer.IKeyGenerator;
import com.baomidou.mybatisplus.core.toolkit.*;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.builder.StaticSqlSource;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.logging.Log;
import org.apache.ibatis.logging.LogFactory;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.reflection.Reflector;
import org.apache.ibatis.reflection.ReflectorFactory;
import org.apache.ibatis.session.Configuration;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import static java.util.stream.Collectors.toList;
/**
* <p>
* 实体类反射表辅助类
* </p>
*
* @author hubin sjy
* @since 2016-09-09
*/
public class TableInfoHelper {
private static final Log logger = LogFactory.getLog(TableInfoHelper.class);
/**
* 储存反射类表信息
*/
private static final Map<Class<?>, TableInfo> TABLE_INFO_CACHE = new ConcurrentHashMap<>();
/**
* 默认表主键名称
*/
private static final String DEFAULT_ID_NAME = "id";
/**
* <p>
* 获取实体映射表信息
* </p>
*
* @param clazz 反射实体类
* @return 数据库表反射信息
*/
public static TableInfo getTableInfo(Class<?> clazz) {
if (clazz == null
|| ReflectionKit.isPrimitiveOrWrapper(clazz)
|| clazz == String.class) {
return null;
}
// https://github.com/baomidou/mybatis-plus/issues/299
TableInfo tableInfo = TABLE_INFO_CACHE.get(ClassUtils.getUserClass(clazz));
if (null != tableInfo) {
return tableInfo;
}
//尝试获取父类缓存
Class<?> currentClass = clazz;
while (null == tableInfo && Object.class != currentClass) {
currentClass = currentClass.getSuperclass();
tableInfo = TABLE_INFO_CACHE.get(ClassUtils.getUserClass(currentClass));
}
if (tableInfo != null) {
TABLE_INFO_CACHE.put(ClassUtils.getUserClass(clazz), tableInfo);
}
return tableInfo;
}
/**
* <p>
* 获取所有实体映射表信息
* </p>
*
* @return 数据库表反射信息集合
*/
@SuppressWarnings("unused")
public static List<TableInfo> getTableInfos() {
return new ArrayList<>(TABLE_INFO_CACHE.values());
}
/**
* <p>
* 实体类反射获取表信息【初始化】
* </p>
*
* @param clazz 反射实体类
* @return 数据库表反射信息
*/
public synchronized static TableInfo initTableInfo(MapperBuilderAssistant builderAssistant, Class<?> clazz) {
TableInfo tableInfo = TABLE_INFO_CACHE.get(clazz);
if (tableInfo != null) {
if (builderAssistant != null) {
tableInfo.setConfiguration(builderAssistant.getConfiguration());
}
return tableInfo;
}
/* 没有获取到缓存信息,则初始化 */
tableInfo = new TableInfo(clazz);
GlobalConfig globalConfig;
if (null != builderAssistant) {
tableInfo.setCurrentNamespace(builderAssistant.getCurrentNamespace());
tableInfo.setConfiguration(builderAssistant.getConfiguration());
globalConfig = GlobalConfigUtils.getGlobalConfig(builderAssistant.getConfiguration());
} else {
// 兼容测试场景
globalConfig = GlobalConfigUtils.defaults();
}
/* 初始化表名相关 */
final String[] excludeProperty = initTableName(clazz, globalConfig, tableInfo);
List<String> excludePropertyList = excludeProperty != null && excludeProperty.length > 0 ? Arrays.asList(excludeProperty) : Collections.emptyList();
/* 初始化字段相关 */
initTableFields(clazz, globalConfig, tableInfo, excludePropertyList);
/* 放入缓存 */
TABLE_INFO_CACHE.put(clazz, tableInfo);
/* 缓存 lambda */
LambdaUtils.installCache(tableInfo);
/* 自动构建 resultMap */
tableInfo.initResultMapIfNeed();
return tableInfo;
}
/**
* <p>
* 初始化 表数据库类型,表名,resultMap
* </p>
*
* @param clazz 实体类
* @param globalConfig 全局配置
* @param tableInfo 数据库表反射信息
* @return 需要排除的字段名
*/
private static String[] initTableName(Class<?> clazz, GlobalConfig globalConfig, TableInfo tableInfo) {
/* 数据库全局配置 */
GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig();
TableName table = clazz.getAnnotation(TableName.class);
String tableName = clazz.getSimpleName();
String tablePrefix = dbConfig.getTablePrefix();
String schema = dbConfig.getSchema();
boolean tablePrefixEffect = true;
String[] excludeProperty = null;
if (table != null) {
if (StringUtils.isNotBlank(table.value())) {
tableName = table.value();
if (StringUtils.isNotBlank(tablePrefix) && !table.keepGlobalPrefix()) {
tablePrefixEffect = false;
}
} else {
tableName = initTableNameWithDbConfig(tableName, dbConfig);
}
if (StringUtils.isNotBlank(table.schema())) {
schema = table.schema();
}
/* 表结果集映射 */
if (StringUtils.isNotBlank(table.resultMap())) {
tableInfo.setResultMap(table.resultMap());
}
tableInfo.setAutoInitResultMap(table.autoResultMap());
excludeProperty = table.excludeProperty();
} else {
tableName = initTableNameWithDbConfig(tableName, dbConfig);
}
String targetTableName = tableName;
if (StringUtils.isNotBlank(tablePrefix) && tablePrefixEffect) {
targetTableName = tablePrefix + targetTableName;
}
if (StringUtils.isNotBlank(schema)) {
targetTableName = schema + StringPool.DOT + targetTableName;
}
tableInfo.setTableName(targetTableName);
/* 开启了自定义 KEY 生成器 */
if (null != dbConfig.getKeyGenerator()) {
tableInfo.setKeySequence(clazz.getAnnotation(KeySequence.class));
}
return excludeProperty;
}
/**
* 根据 DbConfig 初始化 表名
*
* @param className 类名
* @param dbConfig DbConfig
* @return 表名
*/
private static String initTableNameWithDbConfig(String className, GlobalConfig.DbConfig dbConfig) {
String tableName = className;
// 开启表名下划线申明
if (dbConfig.isTableUnderline()) {
tableName = StringUtils.camelToUnderline(tableName);
}
// 大写命名判断
if (dbConfig.isCapitalMode()) {
tableName = tableName.toUpperCase();
} else {
// 首字母小写
tableName = StringUtils.firstToLowerCase(tableName);
}
return tableName;
}
/**
* <p>
* 初始化 表主键,表字段
* </p>
*
* @param clazz 实体类
* @param globalConfig 全局配置
* @param tableInfo 数据库表反射信息
*/
public static void initTableFields(Class<?> clazz, GlobalConfig globalConfig, TableInfo tableInfo, List<String> excludeProperty) {
/* 数据库全局配置 */
GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig();
ReflectorFactory reflectorFactory = tableInfo.getConfiguration().getReflectorFactory();
//TODO @咩咩 有空一起来撸完这反射模块.
Reflector reflector = reflectorFactory.findForClass(clazz);
List<Field> list = getAllFields(clazz);
// 标记是否读取到主键
boolean isReadPK = false;
// 是否存在 @TableId 注解
boolean existTableId = isExistTableId(list);
List<TableFieldInfo> fieldList = new ArrayList<>(list.size());
for (Field field : list) {
if (excludeProperty.contains(field.getName())) {
continue;
}
/* 主键ID 初始化 */
if (existTableId) {
TableId tableId = field.getAnnotation(TableId.class);
if (tableId != null) {
if (isReadPK) {
throw ExceptionUtils.mpe("@TableId can't more than one in Class: \"%s\".", clazz.getName());
} else {
isReadPK = initTableIdWithAnnotation(dbConfig, tableInfo, field, tableId, reflector);
continue;
}
}
} else if (!isReadPK) {
isReadPK = initTableIdWithoutAnnotation(dbConfig, tableInfo, field, reflector);
if (isReadPK) {
continue;
}
}
/* 有 @TableField 注解的字段初始化 */
if (initTableFieldWithAnnotation(dbConfig, tableInfo, fieldList, field)) {
continue;
}
/* 无 @TableField 注解的字段初始化 */
fieldList.add(new TableFieldInfo(dbConfig, tableInfo, field));
}
/* 检查逻辑删除字段只能有最多一个 */
Assert.isTrue(fieldList.parallelStream().filter(TableFieldInfo::isLogicDelete).count() < 2L,
String.format("@TableLogic can't more than one in Class: \"%s\".", clazz.getName()));
/* 字段列表,不可变集合 */
tableInfo.setFieldList(Collections.unmodifiableList(fieldList));
/* 未发现主键注解,提示警告信息 */
if (!isReadPK) {
logger.warn(String.format("Can not find table primary key in Class: \"%s\".", clazz.getName()));
}
}
/**
* <p>
* 判断主键注解是否存在
* </p>
*
* @param list 字段列表
* @return true 为存在 @TableId 注解;
*/
public static boolean isExistTableId(List<Field> list) {
return list.stream().anyMatch(field -> field.isAnnotationPresent(TableId.class));
}
/**
* <p>
* 主键属性初始化
* </p>
*
* @param dbConfig 全局配置信息
* @param tableInfo 表信息
* @param field 字段
* @param tableId 注解
* @param reflector Reflector
*/
private static boolean initTableIdWithAnnotation(GlobalConfig.DbConfig dbConfig, TableInfo tableInfo,
Field field, TableId tableId, Reflector reflector) {
boolean underCamel = tableInfo.isUnderCamel();
final String property = field.getName();
if (field.getAnnotation(TableField.class) != null) {
logger.warn(String.format("This \"%s\" is the table primary key by @TableId annotation in Class: \"%s\",So @TableField annotation will not work!",
property, tableInfo.getEntityType().getName()));
}
/* 主键策略( 注解 > 全局 ) */
// 设置 Sequence 其他策略无效
if (IdType.NONE == tableId.type()) {
tableInfo.setIdType(dbConfig.getIdType());
} else {
tableInfo.setIdType(tableId.type());
}
/* 字段 */
String column = property;
if (StringUtils.isNotBlank(tableId.value())) {
column = tableId.value();
} else {
// 开启字段下划线申明
if (underCamel) {
column = StringUtils.camelToUnderline(column);
}
// 全局大写命名
if (dbConfig.isCapitalMode()) {
column = column.toUpperCase();
}
}
tableInfo.setKeyRelated(checkRelated(underCamel, property, column))
.setKeyColumn(column)
.setKeyProperty(property)
.setKeyType(reflector.getGetterType(property));
return true;
}
/**
* <p>
* 主键属性初始化
* </p>
*
* @param tableInfo 表信息
* @param field 字段
* @param reflector Reflector
* @return true 继续下一个属性判断,返回 continue;
*/
private static boolean initTableIdWithoutAnnotation(GlobalConfig.DbConfig dbConfig, TableInfo tableInfo,
Field field, Reflector reflector) {
final String property = field.getName();
if (DEFAULT_ID_NAME.equalsIgnoreCase(property)) {
if (field.getAnnotation(TableField.class) != null) {
logger.warn(String.format("This \"%s\" is the table primary key by default name for `id` in Class: \"%s\",So @TableField will not work!",
property, tableInfo.getEntityType().getName()));
}
String column = property;
if (dbConfig.isCapitalMode()) {
column = column.toUpperCase();
}
tableInfo.setKeyRelated(checkRelated(tableInfo.isUnderCamel(), property, column))
.setIdType(dbConfig.getIdType())
.setKeyColumn(column)
.setKeyProperty(property)
.setKeyType(reflector.getGetterType(property));
return true;
}
return false;
}
/**
* <p>
* 字段属性初始化
* </p>
*
* @param dbConfig 数据库全局配置
* @param tableInfo 表信息
* @param fieldList 字段列表
* @return true 继续下一个属性判断,返回 continue;
*/
private static boolean initTableFieldWithAnnotation(GlobalConfig.DbConfig dbConfig, TableInfo tableInfo,
List<TableFieldInfo> fieldList, Field field) {
/* 获取注解属性,自定义字段 */
TableField tableField = field.getAnnotation(TableField.class);
if (null == tableField) {
return false;
}
fieldList.add(new TableFieldInfo(dbConfig, tableInfo, field, tableField));
return true;
}
/**
* <p>
* 判定 related 的值
* </p>
*
* @param underCamel 驼峰命名
* @param property 属性名
* @param column 字段名
* @return related
*/
public static boolean checkRelated(boolean underCamel, String property, String column) {
if (StringUtils.isNotColumnName(column)) {
// 首尾有转义符,手动在注解里设置了转义符,去除掉转义符
column = column.substring(1, column.length() - 1);
}
String propertyUpper = property.toUpperCase(Locale.ENGLISH);
String columnUpper = column.toUpperCase(Locale.ENGLISH);
if (underCamel) {
// 开启了驼峰并且 column 包含下划线
return !(propertyUpper.equals(columnUpper) ||
propertyUpper.equals(columnUpper.replace(StringPool.UNDERSCORE, StringPool.EMPTY)));
} else {
// 未开启驼峰,直接判断 property 是否与 column 相同(全大写)
return !propertyUpper.equals(columnUpper);
}
}
/**
* <p>
* 获取该类的所有属性列表
* </p>
*
* @param clazz 反射类
* @return 属性集合
*/
public static List<Field> getAllFields(Class<?> clazz) {
List<Field> fieldList = ReflectionKit.getFieldList(ClassUtils.getUserClass(clazz));
return fieldList.stream()
.filter(field -> {
/* 过滤注解非表字段属性 */
TableField tableField = field.getAnnotation(TableField.class);
return (tableField == null || tableField.exist());
}).collect(toList());
}
public static KeyGenerator genKeyGenerator(String baseStatementId, TableInfo tableInfo, MapperBuilderAssistant builderAssistant) {
IKeyGenerator keyGenerator = GlobalConfigUtils.getKeyGenerator(builderAssistant.getConfiguration());
if (null == keyGenerator) {
throw new IllegalArgumentException("not configure IKeyGenerator implementation class.");
}
Configuration configuration = builderAssistant.getConfiguration();
//TODO 这里不加上builderAssistant.getCurrentNamespace()的会导致com.baomidou.mybatisplus.core.parser.SqlParserHelper.getSqlParserInfo越(chu)界(gui)
String id = builderAssistant.getCurrentNamespace() + StringPool.DOT + baseStatementId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
ResultMap resultMap = new ResultMap.Builder(builderAssistant.getConfiguration(), id, tableInfo.getKeyType(), new ArrayList<>()).build();
MappedStatement mappedStatement = new MappedStatement.Builder(builderAssistant.getConfiguration(), id,
new StaticSqlSource(configuration, keyGenerator.executeSql(tableInfo.getKeySequence().value())), SqlCommandType.SELECT)
.keyProperty(tableInfo.getKeyProperty())
.resultMaps(Collections.singletonList(resultMap))
.build();
configuration.addMappedStatement(mappedStatement);
return new SelectKeyGenerator(mappedStatement, true);
}
}
我们先看到这一行代码,经测试发现,经过了它 logicDelete 配置正确的前提下,此时会首次发生改变为 true(默认值:false)
/* 初始化字段相关 */
initTableFields(clazz, globalConfig, tableInfo, excludePropertyList);
/**
* <p>
* 初始化 表主键,表字段
* </p>
*
* @param clazz 实体类
* @param globalConfig 全局配置
* @param tableInfo 数据库表反射信息
*/
public static void initTableFields(Class<?> clazz, GlobalConfig globalConfig, TableInfo tableInfo, List<String> excludeProperty) {
// ...
/* 字段列表,不可变集合 */
tableInfo.setFieldList(Collections.unmodifiableList(fieldList));
// ...
}
紧接着这里要关注另一个重点类——TableInfo
/*
* Copyright (c) 2011-2020, baomidou (jobob@qq.com).
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.baomidou.mybatisplus.core.metadata;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.core.MybatisConfiguration;
import com.baomidou.mybatisplus.core.toolkit.*;
import com.baomidou.mybatisplus.core.toolkit.sql.SqlScriptUtils;
import lombok.AccessLevel;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.Accessors;
import org.apache.ibatis.mapping.ResultFlag;
import org.apache.ibatis.mapping.ResultMap;
import org.apache.ibatis.mapping.ResultMapping;
import org.apache.ibatis.session.Configuration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
import static java.util.stream.Collectors.joining;
/**
* 数据库表反射信息
*
* @author hubin
* @since 2016-01-23
*/
@Data
@Setter(AccessLevel.PACKAGE)
@Accessors(chain = true)
public class TableInfo implements Constants {
/**
* 实体类型
*/
private Class<?> entityType;
/**
* 表主键ID 类型
*/
private IdType idType = IdType.NONE;
/**
* 表名称
*/
private String tableName;
/**
* 表映射结果集
*/
private String resultMap;
/**
* 是否是需要自动生成的 resultMap
*/
private boolean autoInitResultMap;
/**
* 是否是自动生成的 resultMap
*/
private boolean initResultMap;
/**
* 主键是否有存在字段名与属性名关联
* <p>true: 表示要进行 as</p>
*/
private boolean keyRelated;
/**
* 表主键ID 字段名
*/
private String keyColumn;
/**
* 表主键ID 属性名
*/
private String keyProperty;
/**
* 表主键ID 属性类型
*/
private Class<?> keyType;
/**
* 表主键ID Sequence
*/
private KeySequence keySequence;
/**
* 表字段信息列表
*/
private List<TableFieldInfo> fieldList;
/**
* 命名空间 (对应的 mapper 接口的全类名)
*/
private String currentNamespace;
/**
* MybatisConfiguration 标记 (Configuration内存地址值)
*/
@Getter
private MybatisConfiguration configuration;
/**
* 是否开启逻辑删除
*/
@Setter(AccessLevel.NONE)
private boolean logicDelete;
/**
* 是否开启下划线转驼峰
*/
private boolean underCamel;
/**
* 缓存包含主键及字段的 sql select
*/
@Setter(AccessLevel.NONE)
@Getter(AccessLevel.NONE)
private String allSqlSelect;
/**
* 缓存主键字段的 sql select
*/
@Setter(AccessLevel.NONE)
@Getter(AccessLevel.NONE)
private String sqlSelect;
/**
* 表字段是否启用了插入填充
*
* @since 3.3.0
*/
@Getter
@Setter(AccessLevel.NONE)
private boolean withInsertFill;
/**
* 表字段是否启用了更新填充
*
* @since 3.3.0
*/
@Getter
@Setter(AccessLevel.NONE)
private boolean withUpdateFill;
/**
* 表字段是否启用了乐观锁
*
* @since 3.3.1
*/
@Getter
@Setter(AccessLevel.NONE)
private boolean withVersion;
/**
* 乐观锁字段
*
* @since 3.3.1
*/
@Getter
@Setter(AccessLevel.NONE)
private TableFieldInfo versionFieldInfo;
public TableInfo(Class<?> entityType) {
this.entityType = entityType;
}
/**
* 获得注入的 SQL Statement
*
* @param sqlMethod MybatisPlus 支持 SQL 方法
* @return SQL Statement
*/
public String getSqlStatement(String sqlMethod) {
return currentNamespace + DOT + sqlMethod;
}
/**
* 设置 Configuration
*/
void setConfiguration(Configuration configuration) {
Assert.notNull(configuration, "Error: You need Initialize MybatisConfiguration !");
this.configuration = (MybatisConfiguration) configuration;
this.underCamel = configuration.isMapUnderscoreToCamelCase();
}
/**
* 是否有主键
*
* @return 是否有
*/
public boolean havePK() {
return StringUtils.isNotBlank(keyColumn);
}
/**
* 获取主键的 select sql 片段
*
* @return sql 片段
*/
public String getKeySqlSelect() {
if (sqlSelect != null) {
return sqlSelect;
}
if (havePK()) {
sqlSelect = keyColumn;
if (keyRelated) {
sqlSelect += (" AS " + keyProperty);
}
} else {
sqlSelect = EMPTY;
}
return sqlSelect;
}
/**
* 获取包含主键及字段的 select sql 片段
*
* @return sql 片段
*/
public String getAllSqlSelect() {
if (allSqlSelect != null) {
return allSqlSelect;
}
allSqlSelect = chooseSelect(TableFieldInfo::isSelect);
return allSqlSelect;
}
/**
* 获取需要进行查询的 select sql 片段
*
* @param predicate 过滤条件
* @return sql 片段
*/
public String chooseSelect(Predicate<TableFieldInfo> predicate) {
String sqlSelect = getKeySqlSelect();
String fieldsSqlSelect = fieldList.stream().filter(predicate)
.map(TableFieldInfo::getSqlSelect).collect(joining(COMMA));
if (StringUtils.isNotBlank(sqlSelect) && StringUtils.isNotBlank(fieldsSqlSelect)) {
return sqlSelect + COMMA + fieldsSqlSelect;
} else if (StringUtils.isNotBlank(fieldsSqlSelect)) {
return fieldsSqlSelect;
}
return sqlSelect;
}
/**
* 获取 insert 时候主键 sql 脚本片段
* <p>insert into table (字段) values (值)</p>
* <p>位于 "值" 部位</p>
*
* @return sql 脚本片段
*/
public String getKeyInsertSqlProperty(final String prefix, final boolean newLine) {
final String newPrefix = prefix == null ? EMPTY : prefix;
if (havePK()) {
if (idType == IdType.AUTO) {
return EMPTY;
}
return SqlScriptUtils.safeParam(newPrefix + keyProperty) + COMMA + (newLine ? NEWLINE : EMPTY);
}
return EMPTY;
}
/**
* 获取 insert 时候主键 sql 脚本片段
* <p>insert into table (字段) values (值)</p>
* <p>位于 "字段" 部位</p>
*
* @return sql 脚本片段
*/
public String getKeyInsertSqlColumn(final boolean newLine) {
if (havePK()) {
if (idType == IdType.AUTO) {
return EMPTY;
}
return keyColumn + COMMA + (newLine ? NEWLINE : EMPTY);
}
return EMPTY;
}
/**
* 获取所有 insert 时候插入值 sql 脚本片段
* <p>insert into table (字段) values (值)</p>
* <p>位于 "值" 部位</p>
*
* <li> 自动选部位,根据规则会生成 if 标签 </li>
*
* @return sql 脚本片段
*/
public String getAllInsertSqlPropertyMaybeIf(final String prefix) {
final String newPrefix = prefix == null ? EMPTY : prefix;
return getKeyInsertSqlProperty(newPrefix, true) + fieldList.stream()
.map(i -> i.getInsertSqlPropertyMaybeIf(newPrefix)).filter(Objects::nonNull).collect(joining(NEWLINE));
}
/**
* 获取 insert 时候字段 sql 脚本片段
* <p>insert into table (字段) values (值)</p>
* <p>位于 "字段" 部位</p>
*
* <li> 自动选部位,根据规则会生成 if 标签 </li>
*
* @return sql 脚本片段
*/
public String getAllInsertSqlColumnMaybeIf() {
return getKeyInsertSqlColumn(true) + fieldList.stream().map(TableFieldInfo::getInsertSqlColumnMaybeIf)
.filter(Objects::nonNull).collect(joining(NEWLINE));
}
/**
* 获取所有的查询的 sql 片段
*
* @param ignoreLogicDelFiled 是否过滤掉逻辑删除字段
* @param withId 是否包含 id 项
* @param prefix 前缀
* @return sql 脚本片段
*/
public String getAllSqlWhere(boolean ignoreLogicDelFiled, boolean withId, final String prefix) {
final String newPrefix = prefix == null ? EMPTY : prefix;
String filedSqlScript = fieldList.stream()
.filter(i -> {
if (ignoreLogicDelFiled) {
return !(isLogicDelete() && i.isLogicDelete());
}
return true;
})
.map(i -> i.getSqlWhere(newPrefix)).filter(Objects::nonNull).collect(joining(NEWLINE));
if (!withId || StringUtils.isBlank(keyProperty)) {
return filedSqlScript;
}
String newKeyProperty = newPrefix + keyProperty;
String keySqlScript = keyColumn + EQUALS + SqlScriptUtils.safeParam(newKeyProperty);
return SqlScriptUtils.convertIf(keySqlScript, String.format("%s != null", newKeyProperty), false)
+ NEWLINE + filedSqlScript;
}
/**
* 获取所有的 sql set 片段
*
* @param ignoreLogicDelFiled 是否过滤掉逻辑删除字段
* @param prefix 前缀
* @return sql 脚本片段
*/
public String getAllSqlSet(boolean ignoreLogicDelFiled, final String prefix) {
final String newPrefix = prefix == null ? EMPTY : prefix;
return fieldList.stream()
.filter(i -> {
if (ignoreLogicDelFiled) {
return !(isLogicDelete() && i.isLogicDelete());
}
return true;
}).map(i -> i.getSqlSet(newPrefix)).filter(Objects::nonNull).collect(joining(NEWLINE));
}
/**
* 获取逻辑删除字段的 sql 脚本
*
* @param startWithAnd 是否以 and 开头
* @param isWhere 是否需要的是逻辑删除值
* @return sql 脚本
*/
public String getLogicDeleteSql(boolean startWithAnd, boolean isWhere) {
if (logicDelete) {
TableFieldInfo field = fieldList.stream().filter(TableFieldInfo::isLogicDelete).findFirst()
.orElseThrow(() -> ExceptionUtils.mpe("can't find the logicFiled from table {%s}", tableName));
String logicDeleteSql = formatLogicDeleteSql(field, isWhere);
if (startWithAnd) {
logicDeleteSql = " AND " + logicDeleteSql;
}
return logicDeleteSql;
}
return EMPTY;
}
/**
* format logic delete SQL, can be overrided by subclass
* github #1386
*
* @param field TableFieldInfo
* @param isWhere true: logicDeleteValue, false: logicNotDeleteValue
* @return
*/
private String formatLogicDeleteSql(TableFieldInfo field, boolean isWhere) {
final String value = isWhere ? field.getLogicNotDeleteValue() : field.getLogicDeleteValue();
if (isWhere) {
if (NULL.equalsIgnoreCase(value)) {
return field.getColumn() + " IS NULL";
} else {
return field.getColumn() + EQUALS + String.format(field.isCharSequence() ? "'%s'" : "%s", value);
}
}
final String targetStr = field.getColumn() + EQUALS;
if (NULL.equalsIgnoreCase(value)) {
return targetStr + NULL;
} else {
return targetStr + String.format(field.isCharSequence() ? "'%s'" : "%s", value);
}
}
/**
* 自动构建 resultMap 并注入(如果条件符合的话)
*/
void initResultMapIfNeed() {
if (autoInitResultMap && null == resultMap) {
String id = currentNamespace + DOT + MYBATIS_PLUS + UNDERSCORE + entityType.getSimpleName();
List<ResultMapping> resultMappings = new ArrayList<>();
if (havePK()) {
ResultMapping idMapping = new ResultMapping.Builder(configuration, keyProperty, keyColumn, keyType)
.flags(Collections.singletonList(ResultFlag.ID)).build();
resultMappings.add(idMapping);
}
if (CollectionUtils.isNotEmpty(fieldList)) {
fieldList.forEach(i -> resultMappings.add(i.getResultMapping(configuration)));
}
ResultMap resultMap = new ResultMap.Builder(configuration, id, entityType, resultMappings).build();
configuration.addResultMap(resultMap);
this.resultMap = id;
}
}
void setFieldList(List<TableFieldInfo> fieldList) {
this.fieldList = fieldList;
fieldList.forEach(i -> {
if (i.isLogicDelete()) {
this.logicDelete = true;
}
if (i.isWithInsertFill()) {
this.withInsertFill = true;
}
if (i.isWithUpdateFill()) {
this.withUpdateFill = true;
}
if (i.isVersion()) {
this.withVersion = true;
this.versionFieldInfo = i;
}
});
}
}
经删减,我们看到这——setFieldList,离赋值 true 真相越来越近
void setFieldList(List<TableFieldInfo> fieldList) {
this.fieldList = fieldList;
fieldList.forEach(i -> {
if (i.isLogicDelete()) {
this.logicDelete = true;
}
if (i.isWithInsertFill()) {
this.withInsertFill = true;
}
if (i.isWithUpdateFill()) {
this.withUpdateFill = true;
}
if (i.isVersion()) {
this.withVersion = true;
this.versionFieldInfo = i;
}
});
}
这两个值判空有没印象,就是我们 application.yml 全局配置 globalConfig 配置的时候赋上的噢~
/**
* 是否启用了逻辑删除
*/
public boolean isLogicDelete() {
return StringUtils.isNotBlank(logicDeleteValue) && StringUtils.isNotBlank(logicNotDeleteValue);
}
到这里,才是我们搞清楚首次 logicDelete = true 的基本逻辑,离真相还需要进一步探索……
继续猜想,既然已经为 true 了,而且在 Allinone 架构中,由于多个服务在同一内存里,这个引用地址变量是不可能中途有变动的,所以排查这种情况。
那么到底是什么导致这个逻辑删失效呢?(logicDelete == false)
此时,我们再引入一个源码类——AbstractSqlInjector
/*
* Copyright (c) 2011-2020, baomidou (jobob@qq.com).
* <p>
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
* <p>
* https://www.apache.org/licenses/LICENSE-2.0
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
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 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];
}
}
我们接着重点来看这个方法——inspectInject
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
// ...
if (CollectionUtils.isNotEmpty(methodList)) {
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.");
}
// ...
}
是不是很熟悉,对的,这就是我们调用之前一直讲的 initTableInfo 方法的入口,为什么要讲这个类呢?我们经过测试分析发现,这行代码执行完的时候,logicDelete 还是为 true 但是……
TableInfoHelper.initTableInfo(builderAssistant, modelClass);
当这行代码执行完之后,logicDelete 首次发生反转为 false,那这个方法我们仔细看会发现,他在做一件事情就是将 BaseMapper 里的所有方法生成模板 SQL
// 循环注入自定义方法
methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
那么现在,这个要回到我们文章标题,既然说明了自定义 BaseMapper ,那我们看下我们自定义的 BaseMapper 搞了些啥?!
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import org.apache.ibatis.annotations.Param;
import java.lang.reflect.Field;
import java.util.Objects;
public interface DBaseMapper<T> extends BaseMapper<T> {
/**
* 根据 entity 条件, 删除记录(物理删除)
*
* @param wrapper 实体对象封装操作类(可以为 null)
*/
int deletePhysically(@Param(Constants.WRAPPER) Wrapper<T> wrapper);
}
继续来看 deletePhysically 方法
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import java.lang.reflect.Field;
/**
* @author Lux Sun
* @date 2022/1/14
*/
public class DeletePhysically extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// (1) DELETE FROM %s WHERE %s
// (2) DELETE FROM t_test WHERE test_del=0 AND (id = ? AND test_del = ?)
DSqlMethod dSqlMethod = DSqlMethod.DELETE;
// 反射修改 logicDelete = false 否则生成 (2) 代码
try {
Field logicDelete = tableInfo.getClass().getDeclaredField("logicDelete");
logicDelete.setAccessible(true);
logicDelete.set(tableInfo, false);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
// 生成 SQL
String sql = String.format(dSqlMethod.getSql(), tableInfo.getTableName(),
sqlWhereEntityWrapper(true, tableInfo),
sqlComment());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
// 第 2 个参数必须和 XxxMapper 的自定义方法名一致
return this.addDeleteMappedStatement(mapperClass, dSqlMethod.getMethod(), sqlSource);
}
}
看到这行,此时此刻的心情,恍然大悟
logicDelete.set(tableInfo, false);
灵魂拷问:为啥 A 服务单独启动并没有失效呢?而伴随 B 服务一起启动的时候失效了!
答案:因为 A 服务注入这个自定义方法的时候,是排在最后(如图所示),导致哪怕它最后 logicDelete 被改为 false 了也不会影响什么,但是 B 服务跟在 A 服务后面启动的时候,因为 DAO 是公共类,又再一次被扫描的时候,B 服务在执行以下源码的时候,因为 logicDelete 在上一个 A 服务最后被赋值为 false,导致此时所有的方法都失去了『逻辑删』特性
public void inspectInject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass) {
// ...
if (CollectionUtils.isNotEmpty(methodList)) {
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.");
}
// ...
}
解决方案
- 因为这行代码是根据 tablinfo 的配置信息来生产最终的模板 SQL,所以只要在用完了之后,马上把原先的 logicDelete 还原即可
// 生成 SQL
String sql = String.format(dSqlMethod.getSql(), tableInfo.getTableName(),
sqlWhereEntityWrapper(true, tableInfo),
sqlComment());
import com.baomidou.mybatisplus.core.injector.AbstractMethod;
import com.baomidou.mybatisplus.core.metadata.TableInfo;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import java.lang.reflect.Field;
/**
* @author Lux Sun
* @date 2022/1/14
*/
public class DeletePhysically extends AbstractMethod {
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
// (1) DELETE FROM %s WHERE %s
// (2) DELETE FROM t_test WHERE test_del=0 AND (id = ? AND test_del = ?)
DSqlMethod dSqlMethod = DSqlMethod.DELETE;
// 反射修改 logicDelete = false 否则生成 (2) 代码
try {
boolean oriLogicDelete = tableInfo.isLogicDelete();
Field logicDelete = tableInfo.getClass().getDeclaredField("logicDelete");
logicDelete.setAccessible(true);
logicDelete.set(tableInfo, false);
// 生成 SQL
String sql = String.format(dSqlMethod.getSql(), tableInfo.getTableName(),
sqlWhereEntityWrapper(true, tableInfo),
sqlComment());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, modelClass);
// 还原原来的逻辑删配置
logicDelete.set(tableInfo, oriLogicDelete);
// 第 2 个参数必须和 XxxMapper 的自定义方法名一致
return this.addDeleteMappedStatement(mapperClass, dSqlMethod.getMethod(), sqlSource);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return null;
}
}