Mybatis 拦截器实现单数据源内多数据库切换

京东云开发者
• 阅读 85

作者:京东保险 王奕龙

物流的分拣业务在某些分拣场地只有一个数据源,因为数据量比较大,将所有数据存在一张表内查询速度慢,也为了做不同设备数据的分库管理,便在这个数据源内创建了多个不同库名但表完全相同的数据库,如下图所示:

Mybatis 拦截器实现单数据源内多数据库切换

现在需要上线报表服务来查询所有数据库中的数据进行统计,那么现在的问题来了,该如何 满足在配置一个数据源的情况下来查询该数据源下不同数据库的数据 呢,借助搜索引擎查到的分库实现大多是借助 Sharding-JDBC 框架,配置多个数据源根据分库算法实现数据源的切换,但是对于只有一个数据源的系统来说,我觉得引入框架再将单个数据源根据不同的库名配置成多个不同的数据源来实现分库查询的逻辑我觉得并不好。

如果我们能在 SQL 执行前将 SQL 中所有的表名前拼接上对应的库名的话,那么就能够实现数据源的切换了,下面我们讲一下使用 JSqlParser 和 Mybatis拦截器 实现该逻辑,借助 JSqlParser 主要是为了解析SQL,找到其中所有的表名进行拼接,如果大家有更好的实现方式,该组件并不是必须的。

实现逻辑

SqlSource 是读取 XML 中 SQL 内容并将其发送给数据库执行的对象,如果我们在执行前能拦截到该对象,并将其中的 SQL 替换掉便达成了我们的目的。 SqlSource 有多种实现,包括常见的DynamicSqlSource。其中包含着必要的执行逻辑,我们需要做的工作便是在这些逻辑执行完之后,对 SQL 进行改造,所以这次实现我们使用了 装饰器模式,在原来的 SqlSource 上套一层,执行完 SqlSource 本身的方法之后对其进行增强,代码如下:

public abstract class AbstractDBNameInterceptor {

    /**
     * SqlSource 的装饰器,作用是增强了 getBoundSql 方法,在基础上增加了动态分库的逻辑
     */
    static class SqlSourceDecorator implements SqlSource {

        /**
         * SQL 字段名称
         */
        private static final String SQL_FIELD_NAME = "sql";

        /**
         * 原本的 sql source
         */
        private final SqlSource sqlSource;

        /**
         * 装饰器进行封装
         */
        public SqlSourceDecorator(SqlSource sqlSource) {
            this.sqlSource = sqlSource;
        }

        @Override
        public BoundSql getBoundSql(Object parameterObject) {
            try {
                // 先生成出未修改前的 SQL
                BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
                // 获取数据库名
                String dbName = getSpecificDBName(parameterObject);
                // 有效才修改
                if (isValid(dbName)) {
                    // 生成需要修改完库名的 SQL
                    String targetSQL = getRequiredSqlWithSpecificDBName(boundSql, dbName);
                    // 更新 SQL
                    updateSql(boundSql, targetSQL);
                }

                return boundSql;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        /**
         * 校验是否为有效库名
         */
        private boolean isValid(String dbName) {
            return StringUtils.isNotEmpty(dbName) && !"null".equals(dbName);
        }

        /**
         * 获取到我们想要的库名的 SQL
         */
        private String getRequiredSqlWithSpecificDBName(BoundSql boundSql, String dbName) throws JSQLParserException {
            String originSql = boundSql.getSql();
            // 获取所有的表名
            Set<String> tables = TablesNamesFinder.findTables(originSql);
            for (String table : tables) {
                originSql = originSql.replaceAll(table, dbName + "." + table);
            }
            return originSql;
        }

        /**
         * 修改 SQL
         */
        private void updateSql(BoundSql boundSql, String sql) throws NoSuchFieldException, IllegalAccessException {
            // 通过反射修改sql语句
            Field field = boundSql.getClass().getDeclaredField(SQL_FIELD_NAME);
            field.setAccessible(true);
            field.set(boundSql, sql);
        }
    }

    // ... 
}

定义了 AbstractDBNameInterceptor 抽象类是为了实现复用,并将 SqlSourceDecorator 装饰器定义为静态内部类,这样的话,将所有逻辑都封装在抽象类内部,之后这部分实现好后研发直接实现抽象类的通用方法即可,不必关注它的内部实现。

结合注释我们解释一下 SqlSourceDecorator 的逻辑,其中用到了 Java 反射相关的操作。首先通过反射获取到 SQL,getSpecificDBName 方法是需要自定义实现的,其中 parameterObject 对象是传到 DAO 层执行查询时的参数,在我们的业务中是能够根据其中的设备相关参数拿到对应的所在库名的,而设备和具体库名的映射关系需要提前初始化好。在获取到具体的库名后执行 getRequiredSqlWithSpecificDBName 方法来将其拼接到表名前,在这里我们使用到了 JSqlParser 的工具类,解析出来所有的表名,执行字符串的替换,最后一步同样是使用反射操作将该参数值再写回去,这样便完成了指定库名的任务。

接下来我们需要看下抽象拦截器中供拦截器复用的方法,如下:

public abstract class AbstractDBNameInterceptor {

    /**
     * SqlSource 字段名称
     */
    private static final String SQL_SOURCE_FIELD_NAME = "sqlSource";

    /**
     * 执行修改数据库名的逻辑
     */
    protected Object updateDBName(Invocation invocation) throws Throwable {
        // 装饰器装饰 SqlSource
        decorateSqlSource((MappedStatement) invocation.getArgs()[0]);
        return invocation.proceed();
    }

    /**
     * 装饰 SqlSource
     */
    private void decorateSqlSource(MappedStatement statement) throws NoSuchFieldException, IllegalAccessException {
        if (!(statement.getSqlSource() instanceof SqlSourceDecorator)) {
            Field sqlSource = statement.getClass().getDeclaredField(SQL_SOURCE_FIELD_NAME);
            sqlSource.setAccessible(true);
            sqlSource.set(statement, new SqlSourceDecorator(statement.getSqlSource()));
        }
    }
}

这个还是比较简单的,只是借助反射机制做了一层“装饰”,查询拦截器实现如下:

@Intercepts({
        @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 SelectDBNameInterceptor extends AbstractDBNameInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return updateDBName(invocation);
    }
}

将其配置到 Mybatis 拦截器中,便能实现数据库动态切换了。

点赞
收藏
评论区
推荐文章
Easter79 Easter79
3年前
sql优化之大数据量分页查询(mysql)
当需要从数据库查询的表有上万条记录的时候,一次性查询所有结果会变得很慢,特别是随着数据量的增加特别明显,这时就需要使用分页查询。对于数据库分页查询,也有很多种方法和优化的点。谈优化前的准备工作为了对下面列举的一些优化进行测试,需要使用已有的一张表作为实际例子。表名:order\_history。描述:某个业务的订单历史表。主要字段
Wesley13 Wesley13
3年前
java 微服务分布式 springcloud vue.js flowable 流程引擎
1.代码生成器:\正反双向\(单表、主表、明细表、树形表,快速开发利器)freemaker模版技术,0个代码不用写,生成完整的一个模块,带页面、建表sql脚本、处理类、service等完整模块2.多数据源:(支持同时连接无数个数据库,可以不同的模块连接不同数的据库)支持N个数据源3.阿里数据库连接池dru
Easter79 Easter79
3年前
springcloud vue 微服务分布式 activiti工作流 前后分离 集成代码生成器 shiro权限
1.代码生成器:\正反双向\(单表、主表、明细表、树形表,快速开发利器)freemaker模版技术,0个代码不用写,生成完整的一个模块,带页面、建表sql脚本、处理类、service等完整模块2.多数据源:(支持同时连接无数个数据库,可以不同的模块连接不同数的据库)支持N个数据源3.阿里数据库连接池dru
皕杰报表的初使用
使用皕杰报表,首先得配置数据库驱动,在首选项里面添加。然后的配置数据源,新建数据源,输入数据源名称,下一步,选择数据源类型,选择数据源的驱动程序,编写url及用户、密码等。单击“检测数据源”按钮,出现“连接成功”表示数据库连接成功。单击“完成”即可,如果不能正确配置,都会无法连接数据库。下来新建报表,如果是想简单的查询,可以新建展现报表,如果需要往数据库插
Wesley13 Wesley13
3年前
SSH实现动态数据源切换,事务场景下使用AOP
上周写代码遇到了切换数据源的问题,在同一个方法中向两个不同数据源做一些操作,但是这个方法使用了事务,所以网上一般动态切换数据源的方法就失效了。框架是spirngmvchibernate,数据库是oracle,连接池druid。一般情况下,操作数据都是在DAO层进行处理。一种办法是使用多个DataSource然后创建多个SessionFa
皕杰报表之填报操作·
“新建智能映射”:新建填报操作填报操作名称:设置这个填报操作的名称数据源:设置这个填报操作映射的数据源点击“下一步”,选择数据库表Schema:选择这个数据库的Schema。表名筛选:查找需要选择的数据表,不区分大小写。勾选需要填报的数据表,点击“下一步”,设置填报字段填报单元格:选择填报操作时,这个字段对应报表中的哪个字段的值。勾选主键:当数据进行填报时
Stella981 Stella981
3年前
Spring Boot 集成 Mybatis 实现双数据源
这里用到了SpringBootMybatisDynamicDataSource配置动态双数据源,可以动态切换数据源实现数据库的读写分离。添加依赖加入Mybatis启动器,这里添加了Druid连接池、Oracle数据库驱动为例。<dependency<groupIdorg.mybatis.spring
Wesley13 Wesley13
3年前
mysql可扩展第二部分
  数据分片主要是将数据按照一定的规则分为几个完全不同的数据集合的方式成为数据分片。数据的切分可以是数据库内的,将数据库中的一张表切分为几个不同的数据库表。也可以是数据库级别的,将数据库中的表划分为多个表,这些表存储在不同的数据库服务器上。该部分主要用来介绍数据库级的数据分片。切分规则将具有相关的数据保存在同一个分片上可以提高数据查询效率。数据库分片的路由规
一种实现Spring动态数据源切换的方法 | 京东云技术团队
1目标不在现有查询代码逻辑上做任何改动,实现dao维度的数据源切换(即表维度)2使用场景节约bdp的集群资源。接入新的宽表时,通常uat验证后就会停止集群释放资源,在对应的查询服务器uat环境时需要查询的是生产库的表数据(uat库表因为bdp实时任务停止,
Mybatis 拦截器实现单数据源内多数据库切换 | 京东物流技术团队
物流的分拣业务在某些分拣场地只有一个数据源,因为数据量比较大,将所有数据存在一张表内查询速度慢,也为了做不同设备数据的分库管理,便在这个数据源内创建了多个不同库名但表完全相同的数据库,如下图所示:现在需要上线报表服务来查询所有数据库中的数据进行统计,那么现