一、背景 工作中曾经遇到一个这样的场景:一个项目下面配置了多个数据库,一个接口的业务要查询的数据可能来源于多个表,而这些表却又分布在不同数据库中,这个时候,就可以通过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文档看到一句话:
文章图片
再看我们拦截器里的一行代码
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拦截器实现数据库切换】这两个方案,可以视情况而定。
推荐阅读
- Mybatis|Mybatis事务管理深入理解
- xml|MyBatis03:连接池及事务控制、xml动态SQL语句、多表操作
- mybatis|MyBatis事务
- 程序员|Java进阶(mysql的事务隔离级别面试题)
- 关于mybatis的 insert into select 命令未结束问题
- springboot配置druid数据源及druid的日志监控
- JAVA|SpringBoot配置druid连接池
- SSM|SpringMVC拦截器 & SpringMVC异常处理
- java|卷积神经网络改进想法初探(备份)