Mybatis|基于Mybatis拦截器实现数据库切换

一、背景 工作中曾经遇到一个这样的场景:一个项目下面配置了多个数据库,一个接口的业务要查询的数据可能来源于多个表,而这些表却又分布在不同数据库中,这个时候,就可以通过Mybatis的拦截器,在sql执行前切换数据库即可。
注意:项目是springboot项目,数据使用的是mysql(没测试过oracle)。
二、实现 2.1 主启动

@SpringBootApplication public class Application { public static ConfigurableApplicationContext applicationContext = null; public static void main(String[] args){ applicationContext = SpringApplication.run(Application.class, args); } }

在主启动类中,获取应用上下文,目的是要用来动态的创建Bean。
2.2 mybatis拦截器
import java.sql.Connection; @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} ), @Signature( type = Executor.class, method = "update", args = {MappedStatement.class, Object.class} ) }) @Component public class MybatisInterceptor implements Interceptor { private static AppLogger appLogger = AppLoggerFactory.getLogger(MybatisInterceptor.class); @Autowired PmgDbUtil pmgDbUtil; @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0]; //id为执行的mapper方法的全路径名,com.cmb.pammng.pmg.manager.PmgManager.addPampmg为该mapper要执行的数据库名 String id = mappedStatement.getId(); Connection connection = Application.applicationContext.getBean("sqlSessionFactory").openSession().getConnection(); if (id.startsWith("com.cmb.pammng.")) { String[] result = id.split("\\."); //切换数据源 connection.setCatalog(result[3]); appLogger.info("数据库切换为:" + result[3]); } else { connection.setCatalog(DB_PMG); //DB_PMG为默认数据名 appLogger.info("数据库切换为:" + DB_PMG); }return invocation.proceed(); } @Override public Object plugin(Object target) { if (target instanceof Executor) { return Plugin.wrap(target, this); } else { return target; } } @Override public void setProperties(Properties properties) {} }

2.3 业务方法
@Override public Payment getPaymentById(Long id) { return paymentDao.getPaymentById(id); }

Mybatis的拦截器技术,可以查查资料了解下。大概原理,就是在程序执行到某个mapper方法是,被拦截器拦截,拦截器对其数据库连接做了修改,使用mapper方法真正执行时,能正确的在目标库执行。
2.4 事务问题 我们思考一个问题,在同是一个事务里含对多个数据库的crud操作,是否能将每个数据的操作统一由一个事务管理?
事实上,采用了上述的切库操作,也是可以的通过@Transactional注解实现统一事务管理的,为什么呢?
这里推荐两个链接,让大家了解一下mybatis事务和spring-mybatis事务:
mybatis:https://mybatis.org/mybatis-3/zh/getting-started.html
mybatis-spring:http://mybatis.org/spring/zh/transactions.html
我们从上述链接中可以了解mybatis的事务管理重要组件,我们也从mybatis-spring文档看到一句话:
Mybatis|基于Mybatis拦截器实现数据库切换
文章图片

再看我们拦截器里的一行代码
Connection connection = Application.applicationContext.getBean("sqlSessionFactory").openSession().getConnection();

可以简单这样理解:一个请求进来,spring为我们提供一个线程安全的session,该线程的所有操作都在此session下完成,如此就能保证业务方法对多个库操作时实现统一事务管理。
示例:
@Transactional public int create(Payment payment,Order order) { orderDao.create(order); //订单库 return paymentDao.create(payment); //支付库 }

总之,想要保证所有操作能实现事务管理,就必须保证这些操作都是在同一session下完成的。
三、拓展 我们已经能发现,上述的拦截器,只能针对于只做单表操作的mapper方法,对于多表操作的mapper方法会可能会出现问题。
问:若是某个mapper方法中的sql语句过于复杂,其关联了多个表进行查询,且这些表来自于不同的库,这如何解决?
解决方案一:在该sql语句上不属于本库的表,全部都给它加上前缀(数据库名.表名)。
解决方案二:将这些表,在本库也创建一份(但可能会出现数据分布错乱的问题)
【Mybatis|基于Mybatis拦截器实现数据库切换】这两个方案,可以视情况而定。

    推荐阅读