Mybatis之通用Mapper动态表名及其原理分析

 更新时间:2023年08月29日 10:51:18   作者:tingmailang  
这篇文章主要介绍了Mybatis之通用Mapper动态表名及其原理分析,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教

一、引言

单表增删改查的重复书写相当冗余,目前为了避免这样的冗余我们会使用通用mapper,但是当遇到表名动态变化的时候,比如按年、月、天分表就需要写常规的增删改查sql,这时候就会失去通用mapper单表不用写sql的优势。

此时可以使用通用Mapper动态拦截器操作表名。

二、使用

1、枚举类

@Getter
public enum TableEnum {
    UNSERVICEDAY("t_mac_unservice_day", "未运营日报"),
    SERVICEDAY("t_mac_service_day", "运营日报"),
    ;
    private String table;
    private String desc;
    TableEnum(String table, String desc) {
        this.table = table;
        this.desc = desc;
    }
    public static TableEnum of(String value) {
        Optional<TableEnum> assetEventEnum = Arrays.stream(TableEnum.values())
                .filter(c -> Objects.equals(c.getTable(),value)).findFirst();
        return assetEventEnum.orElse(null);
    }
}

2、拦截器 

@Configuration
public class MybatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
        dynamicTableNameInnerInterceptor.setTableNameHandler((sql, tableName) -> {
            //表名操作
            TableEnum tableEnum = TableEnum.of(tableName);
            switch (tableEnum) {
                case SERVICEDAY:
                case UNSERVICEDAY:
                    return tableName + CommonConstant.SPLIT_CHAR_ + LocalDateTime.now().minusDays(CommonConstant.ONE).format(DateTimeUtil.YYYYMMDD_FORMATTER);
                default:
                    return tableName;
            }
        });
        //加入Mybatis的拦截器
        interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
        return interceptor;
    }
}

3、部分sql不拦截

虽然大部分的sql都是对当天的表进行操作,但是总有操作不是针对当天的,例如创建、删除表、查询过往数据。

初期走了一些弯路,本来是想使用@MapperScan({"***.domain.mapper"})限制这个拦截配置类的作用范围,将拦截限制在固定路径下,然后将不需要拦截的单独在其他路径下编写。

但是这个拦截器是注册在Mybatis内部,底层还是使用Mybatis的拦截sql机制,所以限制作用范围是不起作用的,具体内容感兴趣的可以看原理分析。

回归正题,那么如果不拦截该sql呢?通过查阅通用Mapper的相关文档了解到有一个注解可以使用。

对于通用Mapper提供的动态表名、行级租户等多种功能都可以进行忽略政策,加在Mapper层的方法上就可以避免拦截。

public @interface InterceptorIgnore {
    /**
     * 行级租户 {@link com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor}
     */
    String tenantLine() default "";
    /**
     * 动态表名 {@link com.baomidou.mybatisplus.extension.plugins.inner.DynamicTableNameInnerInterceptor}
     */
    String dynamicTableName() default "";
    /**
     * 攻击 SQL 阻断解析器,防止全表更新与删除 {@link com.baomidou.mybatisplus.extension.plugins.inner.BlockAttackInnerInterceptor}
     */
    String blockAttack() default "";
    /**
     * 垃圾SQL拦截 {@link com.baomidou.mybatisplus.extension.plugins.inner.IllegalSQLInnerInterceptor}
     */
    String illegalSql() default "";
    /**
     * 数据权限 {@link com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor}
     * <p>
     * 默认关闭,需要注解打开
     */
    String dataPermission() default "1";
    /**
     * 分表 {@link com.baomidou.mybatisplus.extension.plugins.inner.ShardingInnerInterceptor}
     */
    String sharding() default "";
    /**
     * 其他的
     * <p>
     * 格式应该为:  "key"+"@"+可选项[false,true,1,0,on,off]
     * 例如: "xxx@1" 或 "xxx@true" 或 "xxx@on"
     * <p>
     * 如果配置了该属性的注解是注解在 Mapper 上的,则如果该 Mapper 的一部分 Method 需要取反则需要在 Method 上注解并配置此属性为反值
     * 例如: "xxx@1" 在 Mapper 上, 则 Method 上需要 "xxx@0"
     */
    String[] others() default {};
}

4、Mapper

public interface MacUnserviceDayMapper extends BaseMapper<MacUnserviceDayEntity> {
    /**
     * 分页展示
     * @param pageQuery
     * @return
     */
    List<MacUnserviceDayEntity> pageList(@Param("query") PageQueryRequest<MacDayRequestDTO> pageQuery);
    List<MacUnserviceDayEntity> exportList(@Param("query") PageQueryRequest<MacDayRequestDTO> pageQuery);
    /**
     * 获取分页数量.
     *
     * @param pageQuery
     * @return
     */
    int pageCount(@Param("query")PageQueryRequest<MacDayRequestDTO> pageQuery);
    //删除指定表
    @InterceptorIgnore(dynamicTableName = "true")
    int deleteBySelect(@Param("timeSuffix")String timeSuffix);
}

三、原理分析

总体架构如下图

1、Mybatis拦截模式

从下图可以看到Mybatis对于sql方法的拦截,动态表名等拦截器实际上只是注册到了它的局部变量interceptors中,所以在Mybatis统一的拦截机制下,给注册的拦截器设置作用范围也就不会生效了。

@Intercepts(
    {
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
        @Signature(type = StatementHandler.class, method = "getBoundSql", args = {}),
        @Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
        @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
    }
)
public class MybatisPlusInterceptor implements Interceptor {
    @Setter
    private List<InnerInterceptor> interceptors = new ArrayList<>();

2、动态表名拦截器注册

其实只是加载到Mybatis的局部变量中

    public void addInnerInterceptor(InnerInterceptor innerInterceptor) {
        this.interceptors.add(innerInterceptor);
    }

3、拦截器生效

在intercept方法中将记载的拦截器进行遍历

public Object intercept(Invocation invocation) throws Throwable {
    Object target = invocation.getTarget();
    Object[] args = invocation.getArgs();
    if (target instanceof Executor) {
        final Executor executor = (Executor) target;
        Object parameter = args[1];
        boolean isUpdate = args.length == 2;
        MappedStatement ms = (MappedStatement) args[0];
        if (!isUpdate && ms.getSqlCommandType() == SqlCommandType.SELECT) {
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            BoundSql boundSql;
            if (args.length == 4) {
                boundSql = ms.getBoundSql(parameter);
            } else {
                boundSql = (BoundSql) args[5];
            }
            //遍历缓存的拦截器
            for (InnerInterceptor query : interceptors) {
                if (!query.willDoQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql)) {
                    return Collections.emptyList();
                }
               //进入查询的前置方法
                query.beforeQuery(executor, ms, parameter, rowBounds, resultHandler, boundSql);
            }
            CacheKey cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            return executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
        } else if (isUpdate) {
            for (InnerInterceptor update : interceptors) {
                if (!update.willDoUpdate(executor, ms, parameter)) {
                    return -1;
                }
                update.beforeUpdate(executor, ms, parameter);
            }
        }
    } else {
        // StatementHandler
        final StatementHandler sh = (StatementHandler) target;
        // 目前只有StatementHandler.getBoundSql方法args才为null
        if (null == args) {
            for (InnerInterceptor innerInterceptor : interceptors) {
                innerInterceptor.beforeGetBoundSql(sh);
            }
        } else {
            Connection connections = (Connection) args[0];
            Integer transactionTimeout = (Integer) args[1];
            for (InnerInterceptor innerInterceptor : interceptors) {
                innerInterceptor.beforePrepare(sh, connections, transactionTimeout);
            }
        }
    }
    return invocation.proceed();
}

beforeQuery负责是否进行表解析的判断

public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
    //检测是否忽略sql
    if (InterceptorIgnoreHelper.willIgnoreDynamicTableName(ms.getId())) return;
    //进入sql解析
    mpBs.sql(this.changeTable(mpBs.sql()));
}

根据INTERCEPTOR_IGNORE_CACHE中的缓存判断是否进入拦截方法 

public static boolean willIgnore(String id, Function<InterceptorIgnoreCache, Boolean> function) {
    //获取sql方法对应的注解缓存
    InterceptorIgnoreCache cache = INTERCEPTOR_IGNORE_CACHE.get(id);
    if (cache == null) {
        cache = INTERCEPTOR_IGNORE_CACHE.get(id.substring(0, id.lastIndexOf(StringPool.DOT)));
    }
    if (cache != null) {
        //比较缓存检查的的属性,此处是dynamicTableName
        Boolean apply = function.apply(cache);
        return apply != null && apply;
    }
    return false;
}

sql解析,解析出表名进入业务方法。

protected String changeTable(String sql) {
    ExceptionUtils.throwMpe(null == tableNameHandler, "Please implement TableNameHandler processing logic");
    //拆分sql
    TableNameParser parser = new TableNameParser(sql);
    List<TableNameParser.SqlToken> names = new ArrayList<>();
    // 表解析
    parser.accept(names::add);
    StringBuilder builder = new StringBuilder();
    int last = 0;
    for (TableNameParser.SqlToken name : names) {
        int start = name.getStart();
        if (start != last) {
            builder.append(sql, last, start);
            //进入业务方法
            builder.append(tableNameHandler.dynamicTableName(sql, name.getValue()));
        }
        last = name.getEnd();
    }
    if (last != sql.length()) {
        builder.append(sql.substring(last));
    }
    return builder.toString();
}

表解析,获取表名给使用者在业务方法中进行处理。

public void accept(TableNameVisitor visitor) {
    int index = 0;
    String first = tokens.get(index).getValue();
    if (isOracleSpecialDelete(first, tokens, index)) {
    //首字符串是删除,只支持紧跟表名
        visitNameToken(tokens.get(index + 1), visitor);
    } else if (isCreateIndex(first, tokens, index)) {
       //首字符串是创建,只支持创建索引
        visitNameToken(tokens.get(index + 4), visitor);
    } else {
        //遍历所有字符串
        while (hasMoreTokens(tokens, index)) {
            String current = tokens.get(index++).getValue();
            if (isFromToken(current)) {
                //找到from字符串
                processFromToken(tokens, index, visitor);
            } else if (isOnDuplicateKeyUpdate(current, index)) {
                //找到duplicate字符串,后面是不是update字符串
                index = skipDuplicateKeyUpdateIndex(index);
            } else if (concerned.contains(current.toLowerCase())) {
                // 找到table、into、join、using、update字符串,认为后续紧跟表名
                if (hasMoreTokens(tokens, index)) {
                    SqlToken next = tokens.get(index++);
                    visitNameToken(next, visitor);
                }
            }
        }
    }
}

四、总结

对于需要按照业务情况分表的情况有很对,对应的工具也有很多,本文主要是表过期就会进行删除,所以没有必要使用sharding的分区方案,采用了Mybatis的动态拦截。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • java中List去除重复数据的5种方式总结

    java中List去除重复数据的5种方式总结

    这篇文章主要给大家总结介绍了关于java中List去除重复数据的5种方式,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-01-01
  • 排序算法的Java实现全攻略

    排序算法的Java实现全攻略

    这篇文章主要介绍了排序算法的Java实现,包括Collections.sort()的使用以及各种经典算法的Java代码实现方法总结,超级推荐!需要的朋友可以参考下
    2015-08-08
  • SpringBoot如何使用Scala进行开发的实现

    SpringBoot如何使用Scala进行开发的实现

    这篇文章主要介绍了SpringBoot如何使用Scala进行开发的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-12-12
  • springboot集成KoTime的配置过程

    springboot集成KoTime的配置过程

    koTime是一个springboot项目性能分析工具,通过追踪方法调用链路以及对应的运行时长快速定位性能瓶颈,这篇文章主要介绍了springboot集成KoTime,需要的朋友可以参考下
    2022-06-06
  • Java集合框架实战应用完全指南

    Java集合框架实战应用完全指南

    本文总结Java集合框架中ArrayList、LinkedList、HashSet、LinkedHashSet和TreeSet的使用场景,结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧
    2025-09-09
  • Spring Task定时任务每天零点执行一次的操作

    Spring Task定时任务每天零点执行一次的操作

    这篇文章主要介绍了Spring Task定时任务每天零点执行一次的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-09-09
  • Java对接阿里云短信服务保姆级教程(新手秒会)

    Java对接阿里云短信服务保姆级教程(新手秒会)

    这篇文章主要介绍了如何在阿里云上申请短信服务以及如何使用Java代码进行对接,包括申请资质、签名和模板,以及编写Java代码整合成工具类进行调用的步骤,需要的朋友可以参考下
    2024-12-12
  • 将Dubbo服务打包成Jar包的操作步骤

    将Dubbo服务打包成Jar包的操作步骤

    Dubbo 是一款流行的 Java RPC 框架,它提供了高性能、透明化的 RPC 远程服务调用方案,在开发基于 Dubbo 的服务时,我们通常需要将服务代码打包成可发布的 JAR 包,本文将详细介绍如何将 Dubbo 服务打包成 JAR 包,并提供相应的配置和步骤,需要的朋友可以参考下
    2024-12-12
  • Java9中操作和查询本地进程信息的示例详解

    Java9中操作和查询本地进程信息的示例详解

    这篇文章主要为大家详细介绍了Java9中操作和查询本地进程信息的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下
    2024-03-03
  • macOS中搭建Java8开发环境(基于Intel x86 64-bit)

    macOS中搭建Java8开发环境(基于Intel x86 64-bit)

    这篇文章主要介绍了macOS中搭建Java8开发环境(基于Intel x86 64-bit) 的相关资料,需要的朋友可以参考下
    2022-12-12

最新评论