背景
为什么要撸这个源码?
最近在使用mybatis-plus框架,发现逻辑删除和版本号这两个有点问题。
- 必须要使用他的api进行查询,才可以拦截并加上逻辑删除。如果自己写的sql是无法拦截并加上逻辑删除字段的。
- 逻辑删除必须要在字段上加上逻辑删的注解,否则也无法加上该字段。
- 版本号这个必须要加上版本号的注解,否则无法生效。
- 更新时,需要带上原值,如果针对某个字段的更新,就无法携带上这个字段。
- 如果版本号字段为空,就无法更新。
基于这种现象,在B端复杂业务的场景下,是不太好用的。
比如B端的开发人员比较注重业务逻辑,逻辑删、版本号等一般都是通用的一些字段,写sql时,还需要关注这个字段,就会很麻烦,复杂业务场景,mp自带的api很多场景是无法满足的,手写sql是较为普遍的,同时也会偶尔用到API,那么就会导致时而api,时而手写,基础字段时而关心,时而不关心。
另外版本号也是如此,并非所有的场景都是全量更新,更普遍的场景是仅更新某几个字段。如果不携带原数据,就会导致更新时,这个字段不被更新。
综合背景大概就是这样,直接开始撸代码吧,带着问题撸代码,可以让撸代码更为高效!
逻辑删除工作原理
首先进入入口处,以removeById为例,其他的类似
进入removeById后,入参就是一个id,通过id,进行删除。
1. 入口解析
public boolean removeById(Serializable id) {
TableInfo tableInfo = TableInfoHelper.getTableInfo(this.getEntityClass());
return tableInfo.isWithLogicDelete() && tableInfo.isWithUpdateFill() ? this.removeById(id, true) : SqlHelper.retBool(this.getBaseMapper().deleteById(id));
}
通过代码,执行逻辑是这样的
- 获取表信息
- 判断是否为逻辑删和开启了更新填充。
-
- 如果同时开启了逻辑删和更新填充,则调用removeById
- 否则调用mapper中的deleteById
通过这里,给我的第一直观感受,是只有开启了逻辑删除和更新填充两个,才会去逻辑删除,否则就会物理删除。
但具体是怎样的,还需要继续撸代码
2. 表信息获取解析
首先先看下表信息是怎样解析的。
因为第二步的判断,是依据表信息中的两个字段进行判断的,所以表信息的构成需要先了解清楚。
着重看withLogicDelete
和 withUpdateFill
两个字段的填充
2.1. 表信息获取逻辑
public static TableInfo getTableInfo(Class<?> clazz) {
if (clazz == null || clazz.isPrimitive() || SimpleTypeRegistry.isSimpleType(clazz) || clazz.isInterface()) {
return null;
}
// https://github.com/baomidou/mybatis-plus/issues/299
Class<?> targetClass = ClassUtils.getUserClass(clazz);
TableInfo tableInfo = TABLE_INFO_CACHE.get(targetClass);
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(targetClass, tableInfo);
}
return tableInfo;
}
通过代码,大概的逻辑就是获取类型,然后再缓存中通过类型来获取表信息。
如果当前类没有表信息,则通过父类从缓存中拿数据。
所以他的逻辑,只是从缓存中获取表信息,不需要关注太多无用信息,关心核心的点,缓存中,存放着所有的表信息。
2.1.1. 小插曲,mp缓存获取或为空指针?为什么没有进行判断?
另外,这套逻辑中,一个隐藏的点
- 缓存中,一定有所有的类型表信息缓存。如果没有,这里就会拿到null,入口处就会空指针。
核心主逻辑抛npe,在这个节点上,对于使用者是很不友好的,但从没遇到过这种情况,那么基本可以断定,在这种场景下,这里一定不为空,为什么这里一定不为空?
- 调用方
通过代码可以得知,本逻辑中,调用这里的都是基于ServiceImpl
进行调用的。
这个类中有两个泛型,ServiceImpl<M extends BaseMapper<T>, T>
,一个M,一个T。
- M: 自定义的mapper,同时这个mapper需要继承BaseMapepr。BaseMapper有一个泛型,泛型为T。那么就代表着,M和T是有绑定关系的。
- T:DO类型,该类型为Mapper中的泛型,该模型通常和数据库模型有关联,为字段映射。mapper.xml中也有字段的映射配置属性。
基于这种情况,就代表所有的T都会被加载到,起码所有的service中的T都会被加载到,那么只要是在service中调用,那么T就一定存在。
2.2. mp缓存加载机制
通过TABLE_INFO_CACHE
可以找到,向缓存中的put机制,是在 TableInfoHelper#initTableInfo()
中进行的。
2.2.1. 大逻辑
/**
* <p>
* 实体类反射获取表信息【初始化】
* </p>
*
* @param clazz 反射实体类
* @return 数据库表反射信息
*/
public static synchronized TableInfo initTableInfo(MapperBuilderAssistant builderAssistant, Class<?> clazz) {
TableInfo targetTableInfo = TABLE_INFO_CACHE.get(clazz);
final Configuration configuration = builderAssistant.getConfiguration();
if (targetTableInfo != null) {
Configuration oldConfiguration = targetTableInfo.getConfiguration();
if (!oldConfiguration.equals(configuration)) {
// 不是同一个 Configuration,进行重新初始化
targetTableInfo = initTableInfo(configuration, builderAssistant.getCurrentNamespace(), clazz);
}
return targetTableInfo;
}
return initTableInfo(configuration, builderAssistant.getCurrentNamespace(), clazz);
}
通过源码可以看到,这个方法是加锁的,所以不会存在并发问题。
大概得流程,就是从缓存中获取,如果缓存中存在,就直接返回。如果不存在,则初始化。
2.2.2. 初始化逻辑
/**
* <p>
* 实体类反射获取表信息【初始化】
* </p>
*
* @param clazz 反射实体类
* @return 数据库表反射信息
*/
private static synchronized TableInfo initTableInfo(Configuration configuration, String currentNamespace, Class<?> clazz) {
GlobalConfig globalConfig = GlobalConfigUtils.getGlobalConfig(configuration);
PostInitTableInfoHandler postInitTableInfoHandler = globalConfig.getPostInitTableInfoHandler();
/* 没有获取到缓存信息,则初始化 */
TableInfo tableInfo = postInitTableInfoHandler.creteTableInfo(configuration, clazz);
tableInfo.setCurrentNamespace(currentNamespace);
/* 初始化表名相关 */
final String[] excludeProperty = initTableName(clazz, globalConfig, tableInfo);
List<String> excludePropertyList = excludeProperty != null && excludeProperty.length > 0 ? Arrays.asList(excludeProperty) : Collections.emptyList();
/* 初始化字段相关 */
initTableFields(configuration, clazz, globalConfig, tableInfo, excludePropertyList);
/* 自动构建 resultMap */
tableInfo.initResultMapIfNeed();
postInitTableInfoHandler.postTableInfo(tableInfo, configuration);
TABLE_INFO_CACHE.put(clazz, tableInfo);
TABLE_NAME_INFO_CACHE.put(tableInfo.getTableName(), tableInfo);
/* 缓存 lambda */
LambdaUtils.installCache(tableInfo);
return tableInfo;
}
解析
- 首先获取全局配置,通过全局配置和当前实体类类型,创建一个基础的TableInfo。
- 初始化表名相关的配置
- 初始化字段相关的配置
- 构建resultMap(表字段和类字段的映射关系)
- 将tableInfo放入缓存
通过这里,可以看出来,TableInfo信息,在这里就已经放入缓存,在调用的时候,就是从这个缓存中获取的。
2.2.3. 初始化表名相关的配置
/**
* <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();
AnnotationHandler annotationHandler = globalConfig.getAnnotationHandler();
TableName table = annotationHandler.getAnnotation(clazz, 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;
}
// 表格式化
String tableFormat = dbConfig.getTableFormat();
if (StringUtils.isNotBlank(tableFormat)) {
targetTableName = String.format(tableFormat, targetTableName);
}
// 表追加 schema 信息
if (StringUtils.isNotBlank(schema)) {
targetTableName = schema + StringPool.DOT + targetTableName;
}
tableInfo.setTableName(targetTableName);
/* 开启了自定义 KEY 生成器 */
if (CollectionUtils.isNotEmpty(dbConfig.getKeyGenerators())) {
tableInfo.setKeySequence(annotationHandler.getAnnotation(clazz, KeySequence.class));
}
return excludeProperty;
}
解析
- 获取类上面标注的
TableName
注解 - 通过schema、表名、表前缀等信息,组合成表名。如果注解上面有resultMap(表字段映射关系),就将这里的映射关系覆盖TableInfo中的映射关系。
- 通过配置的忽略属性配置,将某些表字段进行忽略。
- 最终组合成的表名为:schema+'.'+表前缀+表名
2.2.4. 初始化字段相关配置
/**
* <p>
* 初始化 表主键,表字段
* </p>
*
* @param clazz 实体类
* @param globalConfig 全局配置
* @param tableInfo 数据库表反射信息
*/
private static void initTableFields(Configuration configuration, Class<?> clazz, GlobalConfig globalConfig, TableInfo tableInfo, List<String> excludeProperty) {
AnnotationHandler annotationHandler = globalConfig.getAnnotationHandler();
PostInitTableInfoHandler postInitTableInfoHandler = globalConfig.getPostInitTableInfoHandler();
Reflector reflector = tableInfo.getReflector();
List<Field> list = getAllFields(clazz, annotationHandler);
// 标记是否读取到主键
boolean isReadPK = false;
// 是否存在 @TableId 注解
boolean existTableId = isExistTableId(list, annotationHandler);
// 是否存在 @TableLogic 注解
boolean existTableLogic = isExistTableLogic(list, annotationHandler);
List<TableFieldInfo> fieldList = new ArrayList<>(list.size());
for (Field field : list) {
if (excludeProperty.contains(field.getName())) {
continue;
}
boolean isPK = false;
OrderBy orderBy = annotationHandler.getAnnotation(field, OrderBy.class);
boolean isOrderBy = orderBy != null;
/* 主键ID 初始化 */
if (existTableId) {
TableId tableId = annotationHandler.getAnnotation(field, TableId.class);
if (tableId != null) {
if (isReadPK) {
throw ExceptionUtils.mpe("@TableId can't more than one in Class: \"%s\".", clazz.getName());
}
initTableIdWithAnnotation(globalConfig, tableInfo, field, tableId);
isPK = isReadPK = true;
}
} else if (!isReadPK) {
isPK = isReadPK = initTableIdWithoutAnnotation(globalConfig, tableInfo, field);
}
if (isPK) {
if (orderBy != null) {
tableInfo.getOrderByFields().add(new OrderFieldInfo(tableInfo.getKeyColumn(), orderBy.asc(), orderBy.sort()));
}
continue;
}
final TableField tableField = annotationHandler.getAnnotation(field, TableField.class);
/* 有 @TableField 注解的字段初始化 */
if (tableField != null) {
TableFieldInfo tableFieldInfo = new TableFieldInfo(globalConfig, tableInfo, field, tableField, reflector, existTableLogic, isOrderBy);
fieldList.add(tableFieldInfo);
postInitTableInfoHandler.postFieldInfo(tableFieldInfo, configuration);
continue;
}
/* 无 @TableField 注解的字段初始化 */
TableFieldInfo tableFieldInfo = new TableFieldInfo(globalConfig, tableInfo, field, reflector, existTableLogic, isOrderBy);
fieldList.add(tableFieldInfo);
postInitTableInfoHandler.postFieldInfo(tableFieldInfo, configuration);
}
/* 字段列表 */
tableInfo.setFieldList(fieldList);
/* 未发现主键注解,提示警告信息 */
if (!isReadPK) {
logger.warn(String.format("Can not find table primary key in Class: \"%s\".", clazz.getName()));
}
}
解析
- 初始化主键
- 主键排序配置
- 解析
TableField
注解 - 无注解字段初始化
- 如果没有在类中加主键注解,就会在启动时打印告警日志
这个方法里面会依次对主键、加了注解的字段、没加注解的字段进行初始化处理。
处理的逻辑大体分为两类,一类为主键初始化,一类为字段初始化。
2.2.5. 主键初始化逻辑
主键初始化分为两块
- 读取到了主键,进行初始化。
- 没有读到主键,并且主键没有初始化成功
2.2.5.1. 根据主键配置进行初始化
/**
* <p>
* 主键属性初始化
* </p>
*
* @param globalConfig 全局配置信息
* @param tableInfo 表信息
* @param field 字段
* @param tableId 注解
*/
private static void initTableIdWithAnnotation(GlobalConfig globalConfig, TableInfo tableInfo, Field field, TableId tableId) {
GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig();
boolean underCamel = tableInfo.isUnderCamel();
final String property = field.getName();
if (globalConfig.getAnnotationHandler().isAnnotationPresent(field, TableField.class)) {
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();
}
}
final Class<?> keyType = tableInfo.getReflector().getGetterType(property);
if (keyType.isPrimitive()) {
logger.warn(String.format("This primary key of \"%s\" is primitive !不建议如此请使用包装类 in Class: \"%s\"",
property, tableInfo.getEntityType().getName()));
}
if (StringUtils.isEmpty(tableId.value())) {
String columnFormat = dbConfig.getColumnFormat();
if (StringUtils.isNotBlank(columnFormat)) {
column = String.format(columnFormat, column);
}
}
tableInfo.setKeyRelated(checkRelated(underCamel, property, column))
.setKeyColumn(column)
.setKeyProperty(property)
.setKeyType(keyType);
}
解析
- 判断主键字段上面有没有加
TableField
注解,如果有这个注解,会打印告警,加了主键注解的,TableField
注解会失效。 - 主键的生成策略如果未填写,或者配置的为NONE,则将全局配置中的生成策略配置上去。如果注解上面配置了,则使用注解的配置。
- 通过大小写转换策略,将类中的属性名转为对应的表字段名。全大写 或者 转下划线
- 如果使用了基础类型,就会进行告警。如boolean,int,long等。
- 根据全局配置中的字段格式化策略进行格式化字段名
- 将初始化后的主键信息,塞入tableInfo中
这个方法主要针对主键的一些配置进行初始化赋值。
2.2.5.2. 根据默认主键名进行匹配
如果没有配置主键,就会根据字段名进行匹配,匹配名字是否为id
,默认id字段为主键
/**
* <p>
* 主键属性初始化
* </p>
*
* @param globalConfig 全局配置
* @param tableInfo 表信息
* @param field 字段
* @return true 继续下一个属性判断,返回 continue;
*/
private static boolean initTableIdWithoutAnnotation(GlobalConfig globalConfig, TableInfo tableInfo, Field field) {
GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig();
final String property = field.getName();
if (DEFAULT_ID_NAME.equalsIgnoreCase(property)) {
if (globalConfig.getAnnotationHandler().isAnnotationPresent(field, TableField.class)) {
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();
}
final Class<?> keyType = tableInfo.getReflector().getGetterType(property);
if (keyType.isPrimitive()) {
logger.warn(String.format("This primary key of \"%s\" is primitive !不建议如此请使用包装类 in Class: \"%s\"",
property, tableInfo.getEntityType().getName()));
}
String columnFormat = dbConfig.getColumnFormat();
if (StringUtils.isNotBlank(columnFormat)) {
column = String.format(columnFormat, column);
}
tableInfo.setKeyRelated(checkRelated(tableInfo.isUnderCamel(), property, column))
.setIdType(dbConfig.getIdType())
.setKeyColumn(column)
.setKeyProperty(property)
.setKeyType(keyType);
return true;
}
return false;
}
大体逻辑和主键配置相似,移除了主键注解的配置取值过程,并加了一个主判断逻辑,如果该字段不是id,就不进行处理。是id就进行处理。
2.2.6. 字段初始化逻辑
字段的初始化,通过new TableFieldInfo
进行的,new的过程中,会进行初始化。
初始化同时包含
- 字段初始化
- 排序初始化
/**
* 全新的 存在 TableField 注解时使用的构造函数
*/
public TableFieldInfo(GlobalConfig globalConfig, TableInfo tableInfo, Field field, TableField tableField,
Reflector reflector, boolean existTableLogic, boolean isOrderBy) {
this(globalConfig, tableInfo, field, tableField, reflector, existTableLogic);
this.isOrderBy = isOrderBy;
if (isOrderBy) {
initOrderBy(tableInfo, globalConfig.getAnnotationHandler().getAnnotation(field, OrderBy.class));
}
}
/**
* 全新的 存在 TableField 注解时使用的构造函数
*/
@SuppressWarnings({"unchecked", "rawtypes"})
public TableFieldInfo(GlobalConfig globalConfig, TableInfo tableInfo, Field field, TableField tableField,
Reflector reflector, boolean existTableLogic) {
GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig();
field.setAccessible(true);
this.field = field;
this.version = globalConfig.getAnnotationHandler().isAnnotationPresent(field, Version.class);
this.property = field.getName();
this.propertyType = reflector.getGetterType(this.property);
this.isPrimitive = this.propertyType.isPrimitive();
this.isCharSequence = StringUtils.isCharSequence(this.propertyType);
this.fieldFill = tableField.fill();
this.withInsertFill = this.fieldFill == FieldFill.INSERT || this.fieldFill == FieldFill.INSERT_UPDATE;
this.withUpdateFill = this.fieldFill == FieldFill.UPDATE || this.fieldFill == FieldFill.INSERT_UPDATE;
this.update = tableField.update();
JdbcType jdbcType = tableField.jdbcType();
final Class<? extends TypeHandler> typeHandler = tableField.typeHandler();
final String numericScale = tableField.numericScale();
boolean needAs = false;
String el = this.property;
if (StringUtils.isNotBlank(tableField.property())) {
el = tableField.property();
needAs = true;
}
if (JdbcType.UNDEFINED != jdbcType) {
this.jdbcType = jdbcType;
el += (COMMA + SqlScriptUtils.mappingJdbcType(jdbcType));
}
if (UnknownTypeHandler.class != typeHandler) {
this.typeHandler = (Class<? extends TypeHandler<?>>) typeHandler;
if (tableField.javaType()) {
String javaType = null;
TypeAliasRegistry registry = tableInfo.getConfiguration().getTypeAliasRegistry();
Map<String, Class<?>> typeAliases = registry.getTypeAliases();
for (Map.Entry<String, Class<?>> entry : typeAliases.entrySet()) {
if (entry.getValue().equals(propertyType)) {
javaType = entry.getKey();
break;
}
}
if (javaType == null) {
javaType = propertyType.getName();
registry.registerAlias(javaType, propertyType);
}
el += (COMMA + "javaType=" + javaType);
}
el += (COMMA + SqlScriptUtils.mappingTypeHandler(this.typeHandler));
}
if (StringUtils.isNotBlank(numericScale)) {
el += (COMMA + SqlScriptUtils.mappingNumericScale(Integer.valueOf(numericScale)));
}
this.el = el;
int index = el.indexOf(COMMA);
this.mapping = index > 0 ? el.substring(++index) : null;
this.initLogicDelete(globalConfig, field, existTableLogic);
String column = tableField.value();
if (StringUtils.isBlank(column)) {
column = this.property;
if (tableInfo.isUnderCamel()) {
/* 开启字段下划线申明 */
column = StringUtils.camelToUnderline(column);
}
if (dbConfig.isCapitalMode()) {
/* 开启字段全大写申明 */
column = column.toUpperCase();
}
}
String columnFormat = dbConfig.getColumnFormat();
if (StringUtils.isNotBlank(columnFormat) && tableField.keepGlobalFormat()) {
column = String.format(columnFormat, column);
}
this.column = column;
this.sqlSelect = column;
if (needAs) {
// 存在指定转换属性
String propertyFormat = dbConfig.getPropertyFormat();
if (StringUtils.isBlank(propertyFormat)) {
propertyFormat = "%s";
}
this.sqlSelect += (AS + String.format(propertyFormat, tableField.property()));
} else if (tableInfo.getResultMap() == null && !tableInfo.isAutoInitResultMap() &&
TableInfoHelper.checkRelated(tableInfo.isUnderCamel(), this.property, this.column)) {
/* 未设置 resultMap 也未开启自动构建 resultMap, 字段规则又不符合 mybatis 的自动封装规则 */
String propertyFormat = dbConfig.getPropertyFormat();
String asProperty = this.property;
if (StringUtils.isNotBlank(propertyFormat)) {
asProperty = String.format(propertyFormat, this.property);
}
this.sqlSelect += (AS + asProperty);
}
this.insertStrategy = this.chooseFieldStrategy(tableField.insertStrategy(), dbConfig.getInsertStrategy());
this.updateStrategy = this.chooseFieldStrategy(tableField.updateStrategy(), dbConfig.getUpdateStrategy());
this.whereStrategy = this.chooseFieldStrategy(tableField.whereStrategy(), dbConfig.getWhereStrategy());
if (StringUtils.isNotBlank(tableField.condition())) {
// 细粒度条件控制
this.condition = tableField.condition();
}
// 字段是否注入查询
this.select = tableField.select();
}
这块代码的核心逻辑,就是字段的初始化,这里涉及到了主逻辑中的解析字段,重点关注
这段代码,核心进行解析:逻辑删除、乐观锁、字段配置、填充策略。
- 是否基础类型
- 是否为CharSequence类型
- 是否插入时填充
- 是否更新时填充
- 更新语句时填充字段
- 别名配置
- 类型映射
- 初始化逻辑删除
- 字段格式化策略
- 条件控制
- 查询控制
总的来说,这段代码,通过解析注解中的属性,将字段进行完全的分析,将一些配置、策略、别名、格式化等内容,放入TableFieldInfo配置中,在拿到TableInfo的时候,也就拿到了所有的TableFieldInfo。
通过这里就可以知道,后面如果要写什么切面处理组件时,需要用到表信息,是可以获取到全量的表详细信息的。
2.2.6.1. 逻辑删除处理逻辑
通过逻辑删除字段注解或者全局逻辑删除配置,进行逻辑删除处理。
/**
* 逻辑删除初始化
*
* @param globalConfig 全局配置
* @param field 字段属性对象
*/
private void initLogicDelete(GlobalConfig globalConfig, Field field, boolean existTableLogic) {
GlobalConfig.DbConfig dbConfig = globalConfig.getDbConfig();
/* 获取注解属性,逻辑处理字段 */
TableLogic tableLogic = globalConfig.getAnnotationHandler().getAnnotation(field, TableLogic.class);
if (null != tableLogic) {
if (StringUtils.isNotBlank(tableLogic.value())) {
this.logicNotDeleteValue = tableLogic.value();
} else {
this.logicNotDeleteValue = dbConfig.getLogicNotDeleteValue();
}
if (StringUtils.isNotBlank(tableLogic.delval())) {
this.logicDeleteValue = tableLogic.delval();
} else {
this.logicDeleteValue = dbConfig.getLogicDeleteValue();
}
this.logicDelete = true;
} else if (!existTableLogic) {
String deleteField = dbConfig.getLogicDeleteField();
if (StringUtils.isNotBlank(deleteField) && this.property.equals(deleteField)) {
this.logicNotDeleteValue = dbConfig.getLogicNotDeleteValue();
this.logicDeleteValue = dbConfig.getLogicDeleteValue();
this.logicDelete = true;
}
}
}
解析
- 获取字段上面的逻辑删除注解
- 如果注解存在
-
- 通过逻辑删除注解,获取默认的删除和非删除字段值。
- 如果注解中没有配置,则从全局配置中获取值
- 如果注解不存在,并且全部字段中不包含逻辑删除字段
-
- 从全局中获取逻辑删除字段的删除和非删除值
核心逻辑,就是将逻辑删除的配置加载进内存中。注解的配置值优先级大于全局配置。
2.2.7. 总结
至此,mp将所有表数据缓存的逻辑已经全部完成了。
回想2.1.1中所说,这里一定会有值,那么这里一定是全部的表吗?
整合一下这个方法的调用链
2.2.7.1. mapper调用链
org.apache.ibatis.session.Configuration#parsePendingMethods
↓
com.baomidou.mybatisplus.core.InjectorResolver#resolve
↓
com.baomidou.mybatisplus.core.MybatisMapperAnnotationBuilder#parserInjector
↓
com.baomidou.mybatisplus.core.injector.AbstractSqlInjector#inspectInject
↓
com.baomidou.mybatisplus.core.metadata.TableInfoHelper#initTableInfo(org.apache.ibatis.builder.MapperBuilderAssistant, java.lang.Class<?>)
上面这是第一条调用链,是mybatis官方透出的一个spi接口,这里会扫描所有的mapper进行加载处理,调用到这个方法。所以这里不一定是库里面所有的表,但一定是服务用到的所有表。
具体如何调用的,不在本篇文章的重点内,先不进行扩散,感兴趣的同学可自行撸源码。
2.2.7.2. 数据源调用链
org.apache.ibatis.session.SqlSessionManager#newInstance(java.io.Reader)
↓
com.baomidou.mybatisplus.core.MybatisSqlSessionFactoryBuilder#build(java.io.Reader, java.lang.String, java.util.Properties)
↓
com.baomidou.mybatisplus.core.MybatisXMLConfigBuilder#parse
↓
com.baomidou.mybatisplus.core.MybatisXMLConfigBuilder#parseConfiguration
↓
com.baomidou.mybatisplus.core.MybatisXMLConfigBuilder#mappersElement
↓
com.baomidou.mybatisplus.core.MybatisMapperRegistry#addMapper
↓
com.baomidou.mybatisplus.core.MybatisMapperAnnotationBuilder#parse
通过这条调用链,可以发现,在创建sqlsession时,就会触发这块逻辑,在逻辑中会加载所有的mapper,然后进行逐一解析。
这里最终也是通过mapper进行解析的。
所以只要是能够扫描到的mapper,都能够被解析并缓存。
如果扫描不到的mapper,那当然也是无法使用的,无法注入,也就不存在缓存不缓存的问题了。
3. 调用remove还是delete时的判断逻辑
return tableInfo.isWithLogicDelete() && tableInfo.isWithUpdateFill() ? this.removeById(id, true) : SqlHelper.retBool(this.getBaseMapper().deleteById(id));
获取到表信息后,会根据表信息中的 tableInfo.isWithLogicDelete()
和 tableInfo.isWithUpdateFill()
进行判断,调用remove还是delete。
这两个方法就是表信息中的两个字段的get方法,没有任何逻辑加工。只需要弄清楚这两个字段的赋值逻辑即可。
通过 2.2.4初始化字段相关配置 中的逻辑中,看到当字段被全部初始完成后,就会塞进tableInfo中,在 tableInfo.setFieldList(fieldList);
这个方法中,使用了充血模型,做了一些逻辑处理。
void setFieldList(List<TableFieldInfo> fieldList) {
this.fieldList = fieldList;
AtomicInteger logicDeleted = new AtomicInteger();
AtomicInteger version = new AtomicInteger();
fieldList.forEach(i -> {
if (i.isLogicDelete()) {
this.withLogicDelete = true;
this.logicDeleteFieldInfo = i;
logicDeleted.getAndAdd(1);
}
if (i.isWithInsertFill()) {
this.withInsertFill = true;
}
if (i.isWithUpdateFill()) {
this.withUpdateFill = true;
}
if (i.isVersion()) {
this.withVersion = true;
this.versionFieldInfo = i;
version.getAndAdd(1);
}
});
/* 校验字段合法性 */
Assert.isTrue(logicDeleted.get() <= 1, "@TableLogic not support more than one in Class: \"%s\"", entityType.getName());
Assert.isTrue(version.get() <= 1, "@Version not support more than one in Class: \"%s\"", entityType.getName());
}
通过代码看到,这里会循环所有的字段,判断是否有逻辑删除字段和更新填充字段。有任何一个字段拥有更新填充和逻辑删除属性,这张表就拥有逻辑删除和更新填充的属性。
逻辑删除和更新填充的逻辑在 2.2.6字段初始化逻辑 ,可以再重温一下。
了解了这里,基本就了解了大致的判断原理,只要自己的类中,加了逻辑删除注解,字段填充策略中,有update
或者insert_update
策略,就会触发removeById
,如果缺失任何一个,就会调用deleteById
。
4. removeById 和 deleteById的差异
前面我们认为,removeById是逻辑删除,deleteById是物理删除,实际是不是这样,还需要再验证一下。
先看看removeById吧
4.1. removeById逻辑
public boolean removeById(Serializable id, boolean useFill) {
TableInfo tableInfo = TableInfoHelper.getTableInfo(this.entityClass);
if (useFill && tableInfo.isWithLogicDelete() && !this.entityClass.isAssignableFrom(id.getClass())) {
T instance = tableInfo.newInstance();
Object value = tableInfo.getKeyType() != id.getClass() ? this.conversionService.convert(id, tableInfo.getKeyType()) : id;
tableInfo.setPropertyValue(instance, tableInfo.getKeyProperty(), new Object[]{value});
return this.removeById(instance);
} else {
return SqlHelper.retBool(this.getBaseMapper().deleteById(id));
}
}
通过这个方法,可以看到,里面依然对逻辑删除做了判断,如果是逻辑删除的字段,就会再调用另外一个removeById,否则会调用DeleteById。
再继续深入看removeById
default boolean removeById(T entity) {
return SqlHelper.retBool(this.getBaseMapper().deleteById(entity));
}
嗯...看到再继续深入,这里的removeById,调用的也是deleteById。
那么就可以理解为,上面的一大段逻辑判断,都是废话,调用的都是deleteById,唯一不同的是,一个传入的是id,另一个传入的是实体类。但是实体类中,也只有一个id属性。
那么就当做一个废话吧,我看了最新的mybatis-plus,里面已经移除了这段废话逻辑。
最新的代码是下面这样
@Override
public boolean removeById(Serializable id, boolean useFill) {
return SqlHelper.retBool(getBaseMapper().deleteById(id, useFill));
}
那我们直接看deleteById即可。
4.2. deleteById逻辑
这个方法目前看到明确分为两个方法,一个为 com.baomidou.mybatisplus.core.mapper.BaseMapper#deleteById(java.io.Serializable)
,另外一个为 com.baomidou.mybatisplus.core.mapper.BaseMapper#deleteById(T)
看两个方法几乎一样,入参不同,一个为id,一个为实体。但是方法名都是deleteById。
这两个方法都在com.baomidou.mybatisplus.core.mapper.BaseMapper
,均没有默认实现。
到这里就有点焦头烂额了,不知道下一步会到哪里,可读性变得差了起来。这时候就需要使用到debug了。
这里说一个小技巧,任意做一个断点,只要能断上,然后根据debug时idea左侧显示的栈信息,一步步的往上搜查,就能看到deleteById下一步是什么。这里就是下一步的跳转位置。这里就不做演示了,直接公布答案。
deleteById
下一步就会调用这个方法:com.baomidou.mybatisplus.core.override.MybatisMapperProxy#invoke
接着就会调用mybatis-plus
的核心执行器:com.baomidou.mybatisplus.core.override.MybatisMapperMethod#execute
到这里,我们就可以看到,在执行器中,根据执行的类型进行策略调用。
public Object execute(SqlSession sqlSession, Object[] args) {
Object result;
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
if (IPage.class.isAssignableFrom(method.getReturnType())) {
result = executeForIPage(sqlSession, args);
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
if (result == null && method.getReturnType().isPrimitive() && !method.returnsVoid()) {
throw new BindingException("Mapper method '" + command.getName()
+ " attempted to return null from a method with a primitive return type (" + method.getReturnType() + ").");
}
return result;
}
但麻烦点来了,到这里时,就已经区分了到底为delete还是update。
如果调用:com.baomidou.mybatisplus.core.mapper.BaseMapper#deleteById(java.io.Serializable)
,到这里就是DELETE,执行的就是物理删除。
如果调用: com.baomidou.mybatisplus.core.mapper.BaseMapper#deleteById(T)
到这里就是UPDATE,执行的是逻辑删除。
那么问题点就出在隐藏的位置上,没办法通过撸代码进行查看了。这里就是最核心的逻辑删除的运行策略原理了。
4.2.1. 寻找策略
既然这个是一个代理对象,那么一定有地方通过这个代理对象来为BaseMapper变为代理对象。
还好,这个方法只有一个构造器,通过这个构造器,看引用位置,就可以逆向找到核心位置。
通过构造器的引用,找到了 com.baomidou.mybatisplus.core.override.MybatisMapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)
这个类,这里就是唯一使用这个代理对象的地方。
这个方法的调用也很单一,只有 com.baomidou.mybatisplus.core.MybatisMapperRegistry#getMapper
它进行调用了。那么来分析下这个方法。
@SuppressWarnings("unchecked")
@Override
public <T> T getMapper(Class<T> type, SqlSession sqlSession) {
// fix https://github.com/baomidou/mybatis-plus/issues/4247
MybatisMapperProxyFactory<T> mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.get(type);
if (mapperProxyFactory == null) {
mapperProxyFactory = (MybatisMapperProxyFactory<T>) knownMappers.entrySet().stream()
.filter(t -> t.getKey().getName().equals(type.getName())).findFirst().map(Map.Entry::getValue)
.orElseThrow(() -> new BindingException("Type " + type + " is not known to the MybatisPlusMapperRegistry."));
}
try {
return mapperProxyFactory.newInstance(sqlSession);
} catch (Exception e) {
throw new BindingException("Error getting mapper instance. Cause: " + e, e);
}
}
这个方法名为 getMapper
,根据名字大概可以分析出,获取mapper时,就直接为这个mapper构建了代理对象。
深入逻辑中查看,发现这里是通过type,获取mapper对象,在缓存中找到相同类型的mapper,然后new出来一个代理对象返回。如果没有找到mapper对象就会直接抛出异常。
再继续分析下什么契机会调用这个方法。
org.apache.ibatis.session.SqlSessionManager#getMapper / org.apache.ibatis.session.defaults.DefaultSqlSession#getMapper / org.mybatis.spring.SqlSessionTemplate#getMapper
↓
com.baomidou.mybatisplus.core.MybatisConfiguration#getMapper
↓
org.apache.ibatis.binding.MapperRegistry#getMapper
调用链到这基本就可以明确了,sqlSession的管理器中,会调用该方法,以获取mapper对象。随之就会创建出代理对象返回,对mapper对象进行增强处理。
到这里,基本上一整条的链路都清晰了,知道了什么时候创建的mapper代理对象。
4.2.2. 如何判断执行DELETE还是UPDATE?
细心的人发现,到这里为止,只是说明为什么调用baseMapper会到这里,但是却依然不知道是如何路由到DELETE还是UPDATE的,为什么执行的是delete方法,到这里却走的是UPDATE方法,而且mapper中定义的接口,和这里的类型到底有什么关系?为什么执行一个方法,就知道该执行删除还是更新?
踏上征程,找寻他们之间的关系
4.2.2.1. 定位类型从何而来
switch (command.getType()) {
case INSERT: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.insert(command.getName(), param));
break;
}
case UPDATE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.update(command.getName(), param));
break;
}
case DELETE: {
Object param = method.convertArgsToSqlCommandParam(args);
result = rowCountResult(sqlSession.delete(command.getName(), param));
break;
}
case SELECT:
if (method.returnsVoid() && method.hasResultHandler()) {
executeWithResultHandler(sqlSession, args);
result = null;
} else if (method.returnsMany()) {
result = executeForMany(sqlSession, args);
} else if (method.returnsMap()) {
result = executeForMap(sqlSession, args);
} else if (method.returnsCursor()) {
result = executeForCursor(sqlSession, args);
} else {
// TODO 这里下面改了
if (IPage.class.isAssignableFrom(method.getReturnType())) {
result = executeForIPage(sqlSession, args);
// TODO 这里上面改了
} else {
Object param = method.convertArgsToSqlCommandParam(args);
result = sqlSession.selectOne(command.getName(), param);
if (method.returnsOptional()
&& (result == null || !method.getReturnType().equals(result.getClass()))) {
result = Optional.ofNullable(result);
}
}
}
break;
case FLUSH:
result = sqlSession.flushStatements();
break;
default:
throw new BindingException("Unknown execution method for: " + command.getName());
}
从这里可以看到,比对的type,是Command中获取的。
那么就要看Command从哪来的
4.2.2.2. command来源
public MybatisMapperMethod(Class<?> mapperInterface, Method method, Configuration config) {
this.command = new MapperMethod.SqlCommand(config, mapperInterface, method);
this.method = new MapperMethod.MethodSignature(config, mapperInterface, method);
}
- command是通过构造器传入进来的。
public SqlCommand(Configuration configuration, Class<?> mapperInterface, Method method) {
final String methodName = method.getName();
final Class<?> declaringClass = method.getDeclaringClass();
MappedStatement ms = resolveMappedStatement(mapperInterface, methodName, declaringClass, configuration);
if (ms == null) {
if (method.getAnnotation(Flush.class) == null) {
throw new BindingException(
"Invalid bound statement (not found): " + mapperInterface.getName() + "." + methodName);
}
name = null;
type = SqlCommandType.FLUSH;
} else {
name = ms.getId();
type = ms.getSqlCommandType();
if (type == SqlCommandType.UNKNOWN) {
throw new BindingException("Unknown execution method for: " + name);
}
}
}
- 而command中的type,是通过statement中获取的。
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName, Class<?> declaringClass,
Configuration configuration) {
String statementId = mapperInterface.getName() + "." + methodName;
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
}
if (mapperInterface.equals(declaringClass)) {
return null;
}
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName, declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
statement是通过mapper的类型名字 + 方法名,从configuration中获取的。
private MappedStatement resolveMappedStatement(Class<?> mapperInterface, String methodName, Class<?> declaringClass,
Configuration configuration) {
String statementId = mapperInterface.getName() + "." + methodName;
if (configuration.hasStatement(statementId)) {
return configuration.getMappedStatement(statementId);
}
if (mapperInterface.equals(declaringClass)) {
return null;
}
for (Class<?> superInterface : mapperInterface.getInterfaces()) {
if (declaringClass.isAssignableFrom(superInterface)) {
MappedStatement ms = resolveMappedStatement(superInterface, methodName, declaringClass, configuration);
if (ms != null) {
return ms;
}
}
}
return null;
}
那么,到这里为止,我们就可以确定,type,是statement中配置好的,并且,这个type是和方法做了绑定的。
由此可知,type类型是方法维度的,有映射关系,这也是为什么执行了deleteById,却到了这里的update类型中。
所以,具体执行deleteById为什么走到了Update类型,就需要定位statement的构建逻辑,定位到了statement之后,一切都迎刃而解了。
那么我们继续往上走,看这个statement是什么时候加载进去的,关系的逻辑又是什么。
要想搞清楚,就要先知道configuration是从哪来的。
4.2.2.3. configuration的来源
if (!m.isDefault()) {
return new PlainMethodInvoker(new MybatisMapperMethod(mapperInterface, method, sqlSession.getConfiguration()));
}
通过创建引用地方,可以看到,configuration是从sqlSession中获取到的。
而sqlSession又是MybatisMapperProxy
这个对象的内部属性,并非参数传递过来的。
private final SqlSession sqlSession;
sqlSession属性,由final修饰,说明是这个代理对象创建的时候就已经赋值了。
通过这里,明确是代理对象创建的时候就已经有这个类型的映射关系了,而这个对象的创建时机,由4.2.1寻找策略 可知,这个创建是工程启动的时候就创建了。
创建的时候,调用链错综复杂,单撸代码,无法得到证实,很可能在错误的路上越走越远,所以此时我们就来通过debug进行溯源。
4.2.2.4. debug进行溯源statement的构造过程
- 第一步断点
可以明确的是,一定会到 com.baomidou.mybatisplus.core.override.MybatisMapperProxyFactory#newInstance(org.apache.ibatis.session.SqlSession)
这个方法,那么就在这里断点,然后进行溯源即可。
- 断点分析
断点拦截到之后,看目前的sqlSession状态,发现里面的configuration中,statement已经有值,并且statement中,可以找到deleteById,且已定义sqlCommandType为UPDATE。那么就继续溯源configuration的创建过程,找什么时间往configuration中放入statement。
- 溯源configuration
通过栈帧往上走,走到了 org.mybatis.spring.SqlSessionTemplate
这个类时,发现sqlSession不见了,成为了这个类的属性,说明这个类创建的时候就存入了configuration。
那就需要继续看这个类的创建过程。
由于已经不在一个栈帧中,无法溯源这个类的创建过程,那么就需要重新断点
- 断点
org.mybatis.spring.SqlSessionTemplate
在这个类的所有构造器上面打上断点,然后重启系统。
- 断点分析
断点拦截到之后,发现这个类的入参是一个sqlSessionFactory
,通过工厂来创建的sqlSession
代理对象。而configuration
已经在sqlsessionFactory
中了。
那就需要知道这个sqlSessionFactory
是怎么来的,通过断点,看到sqlSessionFactory
是一个接口,到这里的具体类型为 org.apache.ibatis.session.defaults.DefaultSqlSessionFactory
,那么就重新断点这个类的构建过程。继续溯源
- 断点
org.apache.ibatis.session.defaults.DefaultSqlSessionFactory
在org.apache.ibatis.session.defaults.DefaultSqlSessionFactory
的构造器上面打上断点并重启系统
- 断点分析
通过断点栈帧溯源,找到了configuration的初始构造位置,在com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean#buildSqlSessionFactory
这个方法中。通过configuration关键词,进行撸代码,逐个排查,找到首个构建statement的地方。
如果撸代码能力较差,可以把所有configuration使用地方打上断点,一步一步走,看哪一步执行完后,statement数据被构建出来,这样找会更快一些,多重启几次系统就能定位到核心位置。
这里就不过多演示了,直接公布结果
- 处理statement位置
com.baomidou.mybatisplus.core.injector.AbstractSqlInjector#inspectInject
TableInfo tableInfo = TableInfoHelper.initTableInfo(builderAssistant, modelClass);
List<AbstractMethod> methodList = this.getMethodList(mapperClass, tableInfo);
if (CollectionUtils.isNotEmpty(methodList)) {
// 循环注入自定义方法
methodList.forEach(m -> m.inject(builderAssistant, mapperClass, modelClass, tableInfo));
} else {
logger.debug(mapperClass.toString() + ", No effective injection method was found.");
}
在这里会获取com.baomidou.mybatisplus.core.injector.AbstractMethod 这个类的所有实现类,然后循环进行构建对应的方法。
每个实现类中,都会生成对应的sql来和baseMapper中的sql做对应的映射关系。
4.2.2.5. 映射关系核心源码解读
4.2.2.5.1. getMethodList 方法
public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) {
Stream.Builder<AbstractMethod> builder = Stream.<AbstractMethod>builder()
.add(new Insert())
.add(new Delete())
.add(new Update())
.add(new SelectCount())
.add(new SelectMaps())
.add(new SelectObjs())
.add(new SelectList());
if (tableInfo.havePK()) {
builder.add(new DeleteById())
.add(new DeleteBatchByIds())
.add(new UpdateById())
.add(new SelectById())
.add(new SelectBatchByIds());
} else {
logger.warn(String.format("%s ,Not found @TableId annotation, Cannot use Mybatis-Plus 'xxById' Method.",
tableInfo.getEntityType()));
}
return builder.build().collect(toList());
}
这里看到很简单很粗暴的一种方式,将这些实现类全部new出来,成为一个list,然后在外面进行循环处理。
4.2.2.5.2. inject 方法
/**
* 注入自定义方法
*/
public void inject(MapperBuilderAssistant builderAssistant, Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
this.configuration = builderAssistant.getConfiguration();
this.builderAssistant = builderAssistant;
this.languageDriver = configuration.getDefaultScriptingLanguageInstance();
/* 注入自定义方法 */
injectMappedStatement(mapperClass, modelClass, tableInfo);
}
通过这里发现,所有的类,都会进入这个方法,并没有子类来重写这个方法。核心的差异点就在于injectMappedStatement
方法了。
4.2.2.5.3. injectMappedStatement 方法
通过实现类,发现这里有很多个实现类,并且类均已方法名来命名。
我们核心关注deleteById方法
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql;
SqlMethod sqlMethod;
if (tableInfo.isWithLogicDelete()) {
sqlMethod = SqlMethod.LOGIC_DELETE_BY_ID;
List<TableFieldInfo> fieldInfos = tableInfo.getFieldList().stream()
.filter(TableFieldInfo::isWithUpdateFill)
.filter(f -> !f.isLogicDelete())
.collect(toList());
if (CollectionUtils.isNotEmpty(fieldInfos)) {
String sqlSet = "SET " + SqlScriptUtils.convertIf(fieldInfos.stream()
.map(i -> i.getSqlSet(EMPTY)).collect(joining(EMPTY)), "!@org.apache.ibatis.type.SimpleTypeRegistry@isSimpleType(_parameter.getClass())", true)
+ tableInfo.getLogicDeleteSql(false, false);
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlSet, tableInfo.getKeyColumn(),
tableInfo.getKeyProperty(), tableInfo.getLogicDeleteSql(true, true));
} else {
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlLogicSet(tableInfo),
tableInfo.getKeyColumn(), tableInfo.getKeyProperty(),
tableInfo.getLogicDeleteSql(true, true));
}
SqlSource sqlSource = super.createSqlSource(configuration, sql, Object.class);
return addUpdateMappedStatement(mapperClass, modelClass, methodName, sqlSource);
} else {
sqlMethod = SqlMethod.DELETE_BY_ID;
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), tableInfo.getKeyColumn(),
tableInfo.getKeyProperty());
return this.addDeleteMappedStatement(mapperClass, methodName, super.createSqlSource(configuration, sql, Object.class));
}
}
解析
- 是否开启了逻辑删除
- 如果开启了逻辑删除
-
- 构建sql
- 构建UPDATE类型的statement
- 如果没有开启逻辑删除
-
- 构建sql
- 构建DELETE类型的statement
逻辑很简单,这里也侧面反映了一件事,如果开启了逻辑删除,不管是deleteById(实体类/ id),均为逻辑删除。没开启则均为物理删除,在那里进行控制,无非是对参数的一些处理,并不影响最终的删除逻辑。
这里用到了一个枚举类,这个类中定义了很多很多的sql,通过不同的逻辑,直接硬编码去生成对应的sql。com.baomidou.mybatisplus.core.enums.SqlMethod
/*
* Copyright (c) 2011-2023, baomidou (jobob@qq.com).
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* 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.enums;
/**
* MybatisPlus 支持 SQL 方法
*
* @author hubin
* @since 2016-01-23
*/
public enum SqlMethod {
/**
* 插入
*/
INSERT_ONE("insert", "插入一条数据(选择字段插入)", "<script>\nINSERT INTO %s %s VALUES %s\n</script>"),
UPSERT_ONE("upsert", "Phoenix插入一条数据(选择字段插入)", "<script>\nUPSERT INTO %s %s VALUES %s\n</script>"),
/**
* 删除
*/
DELETE_BY_ID("deleteById", "根据ID 删除一条数据", "DELETE FROM %s WHERE %s=#{%s}"),
@Deprecated
DELETE_BY_MAP("deleteByMap", "根据columnMap 条件删除记录", "<script>\nDELETE FROM %s %s\n</script>"),
DELETE("delete", "根据 entity 条件删除记录", "<script>\nDELETE FROM %s %s %s\n</script>"),
DELETE_BATCH_BY_IDS("deleteBatchIds", "根据ID集合,批量删除数据", "<script>\nDELETE FROM %s WHERE %s IN (%s)\n</script>"),
/**
* 逻辑删除
*/
LOGIC_DELETE_BY_ID("deleteById", "根据ID 逻辑删除一条数据", "<script>\nUPDATE %s %s WHERE %s=#{%s} %s\n</script>"),
LOGIC_DELETE_BY_MAP("deleteByMap", "根据columnMap 条件逻辑删除记录", "<script>\nUPDATE %s %s %s\n</script>"),
LOGIC_DELETE("delete", "根据 entity 条件逻辑删除记录", "<script>\nUPDATE %s %s %s %s\n</script>"),
LOGIC_DELETE_BATCH_BY_IDS("deleteBatchIds", "根据ID集合,批量逻辑删除数据", "<script>\nUPDATE %s %s WHERE %s IN (%s) %s\n</script>"),
/**
* 修改
*/
UPDATE_BY_ID("updateById", "根据ID 选择修改数据", "<script>\nUPDATE %s %s WHERE %s=#{%s} %s\n</script>"),
UPDATE("update", "根据 whereEntity 条件,更新记录", "<script>\nUPDATE %s %s %s %s\n</script>"),
/**
* 逻辑删除 -> 修改
*/
LOGIC_UPDATE_BY_ID("updateById", "根据ID 修改数据", "<script>\nUPDATE %s %s WHERE %s=#{%s} %s\n</script>"),
/**
* 查询
*/
SELECT_BY_ID("selectById", "根据ID 查询一条数据", "SELECT %s FROM %s WHERE %s=#{%s} %s"),
@Deprecated
SELECT_BY_MAP("selectByMap", "根据columnMap 查询一条数据", "<script>SELECT %s FROM %s %s\n</script>"),
SELECT_BATCH_BY_IDS("selectBatchIds", "根据ID集合,批量查询数据", "<script>SELECT %s FROM %s WHERE %s IN (%s) %s </script>"),
@Deprecated
SELECT_ONE("selectOne", "查询满足条件一条数据", "<script>%s SELECT %s FROM %s %s %s\n</script>"),
SELECT_COUNT("selectCount", "查询满足条件总记录数", "<script>%s SELECT COUNT(%s) AS total FROM %s %s %s\n</script>"),
SELECT_LIST("selectList", "查询满足条件所有数据", "<script>%s SELECT %s FROM %s %s %s %s\n</script>"),
@Deprecated
SELECT_PAGE("selectPage", "查询满足条件所有数据(并翻页)", "<script>%s SELECT %s FROM %s %s %s %s\n</script>"),
SELECT_MAPS("selectMaps", "查询满足条件所有数据", "<script>%s SELECT %s FROM %s %s %s %s\n</script>"),
@Deprecated
SELECT_MAPS_PAGE("selectMapsPage", "查询满足条件所有数据(并翻页)", "<script>\n%s SELECT %s FROM %s %s %s %s\n</script>"),
SELECT_OBJS("selectObjs", "查询满足条件所有数据", "<script>%s SELECT %s FROM %s %s %s %s\n</script>");
private final String method;
private final String desc;
private final String sql;
SqlMethod(String method, String desc, String sql) {
this.method = method;
this.desc = desc;
this.sql = sql;
}
public String getMethod() {
return method;
}
public String getDesc() {
return desc;
}
public String getSql() {
return sql;
}
}
可以看到,这里对于deleteById做了两种实现,一种为逻辑删除,执行的实际为UPDATE语句,而普通的删除,则是真正的delete。
5. 查询时的逻辑
通过撸删除时的源码,中间看到有个枚举类:com.baomidou.mybatisplus.core.enums.SqlMethod
枚举类中,定义的有sql,每条sql后面都有一个占位符。%s=#{%s}
,解析sql的时候,会判断表是否开启了逻辑删除,如果开启了,这里会直接解析为 逻辑删除字段 = 默认配置。
这也是为什么只有调用api才能在查询时被自动填充逻辑删除,而手写sql或者lambda表达式都不行,原因是逻辑删除并不是和其他插件一样有拦截器,逻辑删除并没有拦截器,它是直接预编译的sql。
如何定位到的?
通过debug,会发现在进入拦截器之前,sql就已经被编译成了携带逻辑删除的sql了。
感兴趣的可以看下调用链,这里就不再展开了
com.baomidou.mybatisplus.core.override.MybatisMapperMethod#execute
↓
org.mybatis.spring.SqlSessionTemplate#selectOne(java.lang.String, java.lang.Object)
↓
org.apache.ibatis.session.defaults.DefaultSqlSession#selectOne(java.lang.String, java.lang.Object)
↓
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
↓
org.apache.ibatis.session.defaults.DefaultSqlSession#selectList(java.lang.String, java.lang.Object, org.apache.ibatis.session.RowBounds, org.apache.ibatis.session.ResultHandler)
↓
org.apache.ibatis.plugin.Plugin#invoke
↓
com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor#intercept
↓
org.apache.ibatis.mapping.MappedStatement#getBoundSql
↓
org.apache.ibatis.scripting.xmltags.DynamicSqlSource#getBoundSql
↓
org.apache.ibatis.scripting.xmltags.SqlNode#apply
6. 总结
简简单单,又是一篇,至此,逻辑删除的全部工作原理已经完全清晰。
逻辑删除很有用,并且拥有较为强大的能力,但一些细节的处理不足,还有一些特殊的要求是无法满足的,此时就需要屏幕前的你大展拳脚给增强一下了。
基础组件的升级需要谨慎谨慎再谨慎,一不小心,整个公司的系统都会被你全部干瘫痪了。
感悟
读源码到底有没有用处?我理解的源码好处是
- 增加自己的设计思维能力,读了很多的源码,在写某些组件的时候,设计上就要优于很多普通开发人员的设计,由于有大量大佬的设计元素在大脑中,处于混沌状态,当被使用时,就会整合,无形中融入你的设计里面,这就是很大的优势。
- 在使用上,更加的得心应手,比如逻辑删除,如果没读源码,即使很多的大佬,也不能保证使用的绝对正确。全局配置不加注解能不能被使用?加了注解能不能被使用?查询的时候使用api能不能被解析到?手写sql能不能被解析到?lambda表达式的api能被识别吗?等等,很多种情况,只有了解了原理,不需要读文档也知道能不能生效。因为文档是别人嚼过的,是别人灌输过来的,这个是不能用,但不知道为什么不能用,就不会产生很深刻的印象。
- 拓展开发时优势很明显。比如作为基础架构,开发人员让你引入逻辑删除,引入后,开发人员说为什么连表查询不生效?为什么手写的不生效?为什么调用api不生效等?那么你需要对一些公司的特殊需求做兼容增强处理,开源不会为你定制化开发,但是老板需要你定制化增强,此时读过源码的你,就一定可以得心应手,可以很好的去做处理,知道切入点在哪。
乐观锁的工作原理
文章太长了,乐观锁的源码放在下一篇吧~敬请期待。
如果觉得有所帮助,希望可以点赞收藏,感谢感谢,您的认可是对我最大的支持!