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

作者:京东保险 王奕龙

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

在这里插入图片描述

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

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

实现逻辑

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

1public abstract class AbstractDBNameInterceptor { 2 3 /** 4 * SqlSource 的装饰器,作用是增强了 getBoundSql 方法,在基础上增加了动态分库的逻辑 5 */ 6 static class SqlSourceDecorator implements SqlSource { 7 8 /** 9 * SQL 字段名称 10 */ 11 private static final String SQL_FIELD_NAME = "sql"; 12 13 /** 14 * 原本的 sql source 15 */ 16 private final SqlSource sqlSource; 17 18 /** 19 * 装饰器进行封装 20 */ 21 public SqlSourceDecorator(SqlSource sqlSource) { 22 this.sqlSource = sqlSource; 23 } 24 25 @Override 26 public BoundSql getBoundSql(Object parameterObject) { 27 try { 28 // 先生成出未修改前的 SQL 29 BoundSql boundSql = sqlSource.getBoundSql(parameterObject); 30 // 获取数据库名 31 String dbName = getSpecificDBName(parameterObject); 32 // 有效才修改 33 if (isValid(dbName)) { 34 // 生成需要修改完库名的 SQL 35 String targetSQL = getRequiredSqlWithSpecificDBName(boundSql, dbName); 36 // 更新 SQL 37 updateSql(boundSql, targetSQL); 38 } 39 40 return boundSql; 41 } catch (Exception e) { 42 throw new RuntimeException(e); 43 } 44 } 45 46 /** 47 * 校验是否为有效库名 48 */ 49 private boolean isValid(String dbName) { 50 return StringUtils.isNotEmpty(dbName) && !"null".equals(dbName); 51 } 52 53 /** 54 * 获取到我们想要的库名的 SQL 55 */ 56 private String getRequiredSqlWithSpecificDBName(BoundSql boundSql, String dbName) throws JSQLParserException { 57 String originSql = boundSql.getSql(); 58 // 获取所有的表名 59 Set<String> tables = TablesNamesFinder.findTables(originSql); 60 for (String table : tables) { 61 originSql = originSql.replaceAll(table, dbName + "." + table); 62 } 63 return originSql; 64 } 65 66 /** 67 * 修改 SQL 68 */ 69 private void updateSql(BoundSql boundSql, String sql) throws NoSuchFieldException, IllegalAccessException { 70 // 通过反射修改sql语句 71 Field field = boundSql.getClass().getDeclaredField(SQL_FIELD_NAME); 72 field.setAccessible(true); 73 field.set(boundSql, sql); 74 } 75 } 76 77 // ... 78}

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

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

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

1public abstract class AbstractDBNameInterceptor { 2 3 /** 4 * SqlSource 字段名称 5 */ 6 private static final String SQL_SOURCE_FIELD_NAME = "sqlSource"; 7 8 /** 9 * 执行修改数据库名的逻辑 10 */ 11 protected Object updateDBName(Invocation invocation) throws Throwable { 12 // 装饰器装饰 SqlSource 13 decorateSqlSource((MappedStatement) invocation.getArgs()[0]); 14 return invocation.proceed(); 15 } 16 17 /** 18 * 装饰 SqlSource 19 */ 20 private void decorateSqlSource(MappedStatement statement) throws NoSuchFieldException, IllegalAccessException { 21 if (!(statement.getSqlSource() instanceof SqlSourceDecorator)) { 22 Field sqlSource = statement.getClass().getDeclaredField(SQL_SOURCE_FIELD_NAME); 23 sqlSource.setAccessible(true); 24 sqlSource.set(statement, new SqlSourceDecorator(statement.getSqlSource())); 25 } 26 } 27}

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

1@Intercepts({ 2 @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), 3 @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}) 4}) 5public class SelectDBNameInterceptor extends AbstractDBNameInterceptor implements Interceptor { 6 7 @Override 8 public Object intercept(Invocation invocation) throws Throwable { 9 return updateDBName(invocation); 10 } 11}

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

点赞
收藏

评论区

加载中...

相关推荐

sql优化之大数据量分页查询(mysql)

当需要从数据库查询的表有上万条记录的时候,一次性查询所有结果会变得很慢,特别是随着数据量的增加特别明显,这时就需要使用分页查询。对于数据库分页查询,也有很多种方法和优化的点。谈优化前的准备工作为了对下面列举的一些优化进行测试,需要使用已有的一张表作为实际例子。表名:order\_history。描述:某个业务的订单历史表。主要字段

皕杰报表的初使用

使用皕杰报表,首先得配置数据库驱动,在首选项里面添加。然后的配置数据源,新建数据源,输入数据源名称,下一步,选择数据源类型,选择数据源的驱动程序,编写url及用户、密码等。单击“检测数据源”按钮,出现“连接成功”表示数据库连接成功。单击“完成”即可,如果不能正确配置,都会无法连接数据库。下来新建报表,如果是想简单的查询,可以新建展现报表,如果需要往数据库插

springcloud vue 微服务分布式 activiti工作流 前后分离 集成代码生成器 shiro权限

1.代码生成器:\正反双向\(单表、主表、明细表、树形表,快速开发利器)freemaker模版技术,0个代码不用写,生成完整的一个模块,带页面、建表sql脚本、处理类、service等完整模块2.多数据源:(支持同时连接无数个数据库,可以不同的模块连接不同数的据库)支持N个数据源3.阿里数据库连接池dru

java 微服务分布式 springcloud vue.js flowable 流程引擎

1.代码生成器:\正反双向\(单表、主表、明细表、树形表,快速开发利器)freemaker模版技术,0个代码不用写,生成完整的一个模块,带页面、建表sql脚本、处理类、service等完整模块2.多数据源:(支持同时连接无数个数据库,可以不同的模块连接不同数的据库)支持N个数据源3.阿里数据库连接池dru

SSH实现动态数据源切换,事务场景下使用AOP

上周写代码遇到了切换数据源的问题,在同一个方法中向两个不同数据源做一些操作,但是这个方法使用了事务,所以网上一般动态切换数据源的方法就失效了。框架是spirngmvchibernate,数据库是oracle,连接池druid。一般情况下,操作数据都是在DAO层进行处理。一种办法是使用多个DataSource然后创建多个SessionFa

皕杰报表之填报操作·

“新建智能映射”:新建填报操作填报操作名称:设置这个填报操作的名称数据源:设置这个填报操作映射的数据源点击“下一步”,选择数据库表Schema:选择这个数据库的Schema。表名筛选:查找需要选择的数据表,不区分大小写。勾选需要填报的数据表,点击“下一步”,设置填报字段填报单元格:选择填报操作时,这个字段对应报表中的哪个字段的值。勾选主键:当数据进行填报时