使用回顾
mbatis-plus的逻辑删除功能,可以通过注解个性化的配置某一张表或几张表开启逻辑删除功能(方式1),也可以通过配置文件,全局配置逻辑删除功能(方式2);根据情况,选择一种方式即可;
注解方式
在标识逻辑删除的字段上添加注解@TableLogic(value = “1”,delval = “0”)即可;
全局配置方式
mybatis-plus:
global-config:
db-config:
# 标识逻辑删除的数据库字段名称
logic-delete-field: deleted
# 表示已逻辑删除的值(默认也是如此)
logic-delete-value: 1
# 表示未逻辑删除的值(默认也是如此)
logic-not-delete-value: 0
然后对应的表只要有deleted字段,通过使用mabatis-plus提供的动态生成的方法,如deleteById等就是逻辑删除了,而不是物理删除。
其中logic-delete-value和logic-not-delete-value可以不配置,默认就分别是1和0
原理浅析
逻辑删除sql模板
这里我们以deleteById这个方法为核心,来分析
使用mp(mabatis-plus简称),我们能免去在xml中编写sql,其实是因为mp在程序启动时,就帮我们把一些常用的增删改查方法对应的sql动态生成了而已;
这些方法都对应着抽象类AbstractMethod的一个具体子类:
AbstractMethod的各个子类分别对应着SqlMethod枚举类中的一个或多个sql模板,
而这些子类的核心逻辑就是将这些sql模板中的%s,按实际提供的表实体类信息,进行填充,
拼装成完整的sql语句,供后续程序使用。
图中被红框 框住的两个模板,就是deleteById这个mp给我们提供的方法实现逻辑删除(对应LOGIC_DELETE_BY_ID,其实是一个更新语句),或者物理删除(DELETE_BY_ID,真正的delete删除语句)的两个模板;
物理删除or逻辑删除?
那么,mp是根据什么选择具体使用哪个模板呢?
deleteById这个方法对应的AbstractMethod子类是DeleteById,
@Override
public MappedStatement injectMappedStatement(Class<?> mapperClass, Class<?> modelClass, TableInfo tableInfo) {
String sql;
// 从SqlMethod枚举类中选择的sql模板
/* 根据前面所说,可知此处sql模板为:
<script>
UPDATE %s %s WHERE %s=#{%s} %s
</script>
*/
SqlMethod sqlMethod = SqlMethod.LOGIC_DELETE_BY_ID;
if (tableInfo.isWithLogicDelete()) {
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), sqlLogicSet(tableInfo),
tableInfo.getKeyColumn(), tableInfo.getKeyProperty(),
tableInfo.getLogicDeleteSql(true, true));
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class);
return addUpdateMappedStatement(mapperClass, modelClass, getMethod(sqlMethod), sqlSource);
} else {
// 从SqlMethod枚举类中选择的sql模板
/* 根据前面所说,可知此处sql模板为:
<script>
DELETE FROM %s WHERE %s=#{%s}
</script>
*/
sqlMethod = SqlMethod.DELETE_BY_ID;
sql = String.format(sqlMethod.getSql(), tableInfo.getTableName(), tableInfo.getKeyColumn(),
tableInfo.getKeyProperty());
SqlSource sqlSource = languageDriver.createSqlSource(configuration, sql, Object.class);
return this.addDeleteMappedStatement(mapperClass, getMethod(sqlMethod), sqlSource);
}
}
然后我们先来debug看看最终将sql模板中%s替换后的sql语句:
我创建的表结构为:
为了突出重点,表中只有id主键,和一个deleted逻辑删除标识字段
对应JAVA实体类为LogicDeleteTest
情况1,进入if分支(逻辑删除):
情况2,进入else分支(物理删除):
可以看出,最终是选择LOGIC_DELETE_BY_ID sql模板还是DELETE_BY_ID sql模板,主要是tableInfo.isWithLogicDelete()这个变量来决定的;
这个isWithLogicDelete方法是获取TableInfo类中的bool类型属性isWithLogicDelete;
那么,isWithLogicDelete又是怎么赋值的呢?
TableInfo的withLogicDelete赋值逻辑
要知道isWithLogicDelete是怎么赋值的,就得看看哪里创建的TableInfo?
我们往上一层,发现是抽象类AbstractMethod的inject方法调用了DeleteById类的injectMappedStatement方法,并传入TableInfo;
继续往上,最终,定位到AbstractSqlInjector类中的inspectInject方法:
这个类是mp的sql注入器,程序启动时,就是通过调用inspectInject方法,帮我们完成了deleteById等方法的自动注入
我们进入TableInfoHelper.initTableInfo(builderAssistant, modelClass)的initTableInfo方法,
省略中间部分方法。。。最终,进入方法TableInfoHelper的initTableInfo方法
这个方法就是根据我们定义的数据库表映射实体类LogicDeleteTest来解析表名,表字段,以及ResultMap等信息
private synchronized static TableInfo initTableInfo(Configuration configuration, String currentNamespace, Class<?> clazz) {
/* 没有获取到缓存信息,则初始化 */
TableInfo tableInfo = new TableInfo(clazz);
tableInfo.setCurrentNamespace(currentNamespace);
tableInfo.setConfiguration(configuration);
GlobalConfig globalConfig = GlobalConfigUtils.getGlobalConfig(configuration);
/* 初始化表名相关 */
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);
/* 自动构建 resultMap */
tableInfo.initResultMapIfNeed();
/* 缓存 lambda */
LambdaUtils.installCache(tableInfo);
return tableInfo;
}
我们只关注其中<初始化字段相关的方法>initTableFields,即解析表字段相关【主要关注其中关于局部变量fieldList的操作,即向其中添加TableFieldInfo成员,以及 tableInfo.setFieldList(fieldList)这行代码】;
private 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);
// 是否存在 @TableLogic 注解
boolean existTableLogic = isExistTableLogic(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 {
initTableIdWithAnnotation(dbConfig, tableInfo, field, tableId, reflector);
isReadPK = true;
continue;
}
}
} else if (!isReadPK) {
isReadPK = initTableIdWithoutAnnotation(dbConfig, tableInfo, field, reflector);
if (isReadPK) {
continue;
}
}
final TableField tableField = field.getAnnotation(TableField.class);
/* 有 @TableField 注解的字段初始化 */
if (tableField != null) {
fieldList.add(new TableFieldInfo(dbConfig, tableInfo, field, tableField, reflector, existTableLogic));
continue;
}
/* 无 @TableField 注解的字段初始化 */
fieldList.add(new TableFieldInfo(dbConfig, tableInfo, field, reflector, existTableLogic));
}
/* 字段列表 */
tableInfo.setFieldList(fieldList);
/* 未发现主键注解,提示警告信息 */
if (!isReadPK) {
logger.warn(String.format("Can not find table primary key in Class: \"%s\".", clazz.getName()));
}
}
在此方法中,就是为我们定义的表映射实体类LogicDeleteTest的每个字段(不包括配置了注解@TableField(exist = false)的字段),创建一个对应的TableFieldInfo类的实例,其核心创建逻辑为【我们只需要关注其中的this.initLogicDelete(dbConfig, field, existTableLogic);这行代码】:
public TableFieldInfo(GlobalConfig.DbConfig dbConfig, TableInfo tableInfo, Field field, Reflector reflector,
boolean existTableLogic) {
field.setAccessible(true);
this.field = field;
this.version = field.getAnnotation(Version.class) != null;
this.property = field.getName();
this.propertyType = reflector.getGetterType(this.property);
this.isPrimitive = this.propertyType.isPrimitive();
this.isCharSequence = StringUtils.isCharSequence(this.propertyType);
this.el = this.property;
this.insertStrategy = dbConfig.getInsertStrategy();
this.updateStrategy = dbConfig.getUpdateStrategy();
this.whereStrategy = dbConfig.getSelectStrategy();
this.initLogicDelete(dbConfig, field, existTableLogic);
String column = this.property;
if (tableInfo.isUnderCamel()) {
/* 开启字段下划线申明 */
column = StringUtils.camelToUnderline(column);
}
if (dbConfig.isCapitalMode()) {
/* 开启字段全大写申明 */
column = column.toUpperCase();
}
String columnFormat = dbConfig.getColumnFormat();
if (StringUtils.isNotBlank(columnFormat)) {
column = String.format(columnFormat, column);
}
this.column = column;
this.sqlSelect = column;
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);
}
}
我们进入initLogicDelete方法,这里就是解析字段上TableLogic注解的核心逻辑了,我们详细看一下
private void initLogicDelete(GlobalConfig.DbConfig dbConfig, Field field, boolean existTableLogic) {
/* 获取注解属性,逻辑处理字段 */
TableLogic tableLogic = field.getAnnotation(TableLogic.class);
if (null != tableLogic) {
// 字段上有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();
}
// 设置当前TableFieldInfo的属性logicDelete=true
// 这个属性是后面赋值TableInfo中isWithLogicDelete属性的关键;
this.logicDelete = true;
} else if (!existTableLogic) {
// existTableLogic是由外面传入的,
// 只要当前当前表映射实体类中任一字段标识了TableLogic,就为true
// 进入此分支,标识当前表映射实体类中所有字段都没有标识TableLogic注解
// 那么再去配置文件中获取logic-delete-field属性
String deleteField = dbConfig.getLogicDeleteField();
// 如果我们在配置中配置了logic-delete-field属性,
// 并且当前TableFieldInfo就是对应着我们配置的逻辑删除标识字段
// 也配置TableFieldInfo的属性logicDelete=true
if (StringUtils.isNotBlank(deleteField) && this.property.equals(deleteField)) {
this.logicNotDeleteValue = dbConfig.getLogicNotDeleteValue();
this.logicDeleteValue = dbConfig.getLogicDeleteValue();
this.logicDelete = true;
}
}
}
然后回到TableInfoHelper的initTableFields方法, 我们通过tableInfo.setFieldList(fieldList)这行代码,进入TableInfo的setFieldList方法,
此方法就是将前面创建的各个TableFieldInfo实例放入TableInfo的List fieldList表字段信息列表属性中:
void setFieldList(List<TableFieldInfo> fieldList) {
this.fieldList = fieldList;
AtomicInteger logicDeleted = new AtomicInteger();
AtomicInteger version = new AtomicInteger();
fieldList.forEach(i -> {
// 这里就是设置TableInfo的withLogicDelete的地方了
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());
}
根据循环fieldList,只要有一个TableFieldInfo的logicDelete为true,就设置TableInfo的withLogicDelete的值为true(当然,根据倒数第二行代码可知,最多也只能有一个TableFieldInfo的logicDelete为true,否则会抛异常)。
至此,已经分析完了mp逻辑删除的原理了,如有错误,欢迎留言。