【撸源码】【mybatis-plus】乐观锁和逻辑删除是如何工作的——上篇

15 篇文章 0 订阅
3 篇文章 0 订阅

背景

为什么要撸这个源码?

最近在使用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. 表信息获取解析

首先先看下表信息是怎样解析的。

因为第二步的判断,是依据表信息中的两个字段进行判断的,所以表信息的构成需要先了解清楚。

着重看withLogicDeletewithUpdateFill两个字段的填充

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不生效等?那么你需要对一些公司的特殊需求做兼容增强处理,开源不会为你定制化开发,但是老板需要你定制化增强,此时读过源码的你,就一定可以得心应手,可以很好的去做处理,知道切入点在哪。

乐观锁的工作原理

文章太长了,乐观锁的源码放在下一篇吧~敬请期待。

如果觉得有所帮助,希望可以点赞收藏,感谢感谢,您的认可是对我最大的支持!

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

写代码的喵o

请作者吃包辣条可好

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值