MyBatis-Plus - 论自定义 BaseMapper 方法『逻辑删』失效解决方案

本文主要是介绍MyBatis-Plus - 论自定义 BaseMapper 方法『逻辑删』失效解决方案,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

问题描述

在上一篇我们讲过 MyBatis-Plus - 论 1 个实体类被 N 个DAO 类绑定,导致 MP 特性(逻辑删)失效的解决方案-CSDN博客

所以在这个基础上,我们可以很好定位到源码的分析位置。

但是今天这个问题就更奇怪了,已经确保 1 个实体类只被 1 个 DAO 类绑定,可还是『逻辑删』失效啊~

于是,又开始苦逼的 Debug Mybatis-Plus 源码……

原因分析

  • 再分析源码前,我们先得出几个现象级的结论
  1. 因为我们采用了 Allinone 的架构,所以才发现的问题
  2. 我们单独启动 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/299TableInfo 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内存地址值)*/@Getterprivate 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);@Overridepublic 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 {@Overridepublic 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();}// 生成 SQLString 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 {@Overridepublic 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);// 生成 SQLString 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;}
}

这篇关于MyBatis-Plus - 论自定义 BaseMapper 方法『逻辑删』失效解决方案的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/485525

相关文章

mybatis的整体架构

mybatis的整体架构分为三层: 1.基础支持层 该层包括:数据源模块、事务管理模块、缓存模块、Binding模块、反射模块、类型转换模块、日志模块、资源加载模块、解析器模块 2.核心处理层 该层包括:配置解析、参数映射、SQL解析、SQL执行、结果集映射、插件 3.接口层 该层包括:SqlSession 基础支持层 该层保护mybatis的基础模块,它们为核心处理层提供了良好的支撑。

【前端学习】AntV G6-08 深入图形与图形分组、自定义节点、节点动画(下)

【课程链接】 AntV G6:深入图形与图形分组、自定义节点、节点动画(下)_哔哩哔哩_bilibili 本章十吾老师讲解了一个复杂的自定义节点中,应该怎样去计算和绘制图形,如何给一个图形制作不间断的动画,以及在鼠标事件之后产生动画。(有点难,需要好好理解) <!DOCTYPE html><html><head><meta charset="UTF-8"><title>06

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

浅谈主机加固,六种有效的主机加固方法

在数字化时代,数据的价值不言而喻,但随之而来的安全威胁也日益严峻。从勒索病毒到内部泄露,企业的数据安全面临着前所未有的挑战。为了应对这些挑战,一种全新的主机加固解决方案应运而生。 MCK主机加固解决方案,采用先进的安全容器中间件技术,构建起一套内核级的纵深立体防护体系。这一体系突破了传统安全防护的局限,即使在管理员权限被恶意利用的情况下,也能确保服务器的安全稳定运行。 普适主机加固措施:

webm怎么转换成mp4?这几种方法超多人在用!

webm怎么转换成mp4?WebM作为一种新兴的视频编码格式,近年来逐渐进入大众视野,其背后承载着诸多优势,但同时也伴随着不容忽视的局限性,首要挑战在于其兼容性边界,尽管WebM已广泛适应于众多网站与软件平台,但在特定应用环境或老旧设备上,其兼容难题依旧凸显,为用户体验带来不便,再者,WebM格式的非普适性也体现在编辑流程上,由于它并非行业内的通用标准,编辑过程中可能会遭遇格式不兼容的障碍,导致操

透彻!驯服大型语言模型(LLMs)的五种方法,及具体方法选择思路

引言 随着时间的发展,大型语言模型不再停留在演示阶段而是逐步面向生产系统的应用,随着人们期望的不断增加,目标也发生了巨大的变化。在短短的几个月的时间里,人们对大模型的认识已经从对其zero-shot能力感到惊讶,转变为考虑改进模型质量、提高模型可用性。 「大语言模型(LLMs)其实就是利用高容量的模型架构(例如Transformer)对海量的、多种多样的数据分布进行建模得到,它包含了大量的先验

【北交大信息所AI-Max2】使用方法

BJTU信息所集群AI_MAX2使用方法 使用的前提是预约到相应的算力卡,拥有登录权限的账号密码,一般为导师组共用一个。 有浏览器、ssh工具就可以。 1.新建集群Terminal 浏览器登陆10.126.62.75 (如果是1集群把75改成66) 交互式开发 执行器选Terminal 密码随便设一个(需记住) 工作空间:私有数据、全部文件 加速器选GeForce_RTX_2080_Ti

自定义类型:结构体(续)

目录 一. 结构体的内存对齐 1.1 为什么存在内存对齐? 1.2 修改默认对齐数 二. 结构体传参 三. 结构体实现位段 一. 结构体的内存对齐 在前面的文章里我们已经讲过一部分的内存对齐的知识,并举出了两个例子,我们再举出两个例子继续说明: struct S3{double a;int b;char c;};int mian(){printf("%zd\n",s

【VUE】跨域问题的概念,以及解决方法。

目录 1.跨域概念 2.解决方法 2.1 配置网络请求代理 2.2 使用@CrossOrigin 注解 2.3 通过配置文件实现跨域 2.4 添加 CorsWebFilter 来解决跨域问题 1.跨域概念 跨域问题是由于浏览器实施了同源策略,该策略要求请求的域名、协议和端口必须与提供资源的服务相同。如果不相同,则需要服务器显式地允许这种跨域请求。一般在springbo