mybatisPlus逻辑删除原理

使用回顾

mbatis-plus的逻辑删除功能,可以通过注解个性化的配置某一张表或几张表开启逻辑删除功能(方式1),也可以通过配置文件,全局配置逻辑删除功能(方式2);根据情况,选择一种方式即可;

注解方式

在标识逻辑删除的字段上添加注解@TableLogic(value = “1”,delval = “0”)即可;
image.png

全局配置方式

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的一个具体子类:
image.png
AbstractMethod的各个子类分别对应着SqlMethod枚举类中的一个或多个sql模板,
而这些子类的核心逻辑就是将这些sql模板中的%s,按实际提供的表实体类信息,进行填充,
拼装成完整的sql语句,供后续程序使用。
image.png
图中被红框 框住的两个模板,就是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语句:

我创建的表结构为:
image.png
为了突出重点,表中只有id主键,和一个deleted逻辑删除标识字段
对应JAVA实体类为LogicDeleteTest

情况1,进入if分支(逻辑删除):
image.png
情况2,进入else分支(物理删除):
image.png
可以看出,最终是选择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等方法的自动注入

image.png
我们进入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逻辑删除的原理了,如有错误,欢迎留言。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值