場景說明
日常開發中,連接配接多個資料庫是一個很常見的需求,我們的系統是基于spring boot+mybatis進行資料庫的操作,網上常見的思路是基于不同的資料庫建立不同的bean,大概的實作方式如下:
package com.joylee.fd.crontab.config;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import javax.sql.DataSource;
@Configuration
@MapperScan(basePackages = "com.joylee.fd.crontab.mapper.distribution", sqlSessionTemplateRef = "distributionSqlSessionTemplate")
public class DistributionDBConfiguration {
@Bean(name = "distributionDataSource")
@ConfigurationProperties(prefix = "spring.datasource")
@Primary
public DataSource distributionDataSource() {
return DataSourceBuilder.create().build();
}
@Bean(name = "distributionSqlSessionFactory")
@Primary
public SqlSessionFactory distributionSqlSessionFactory(@Qualifier("distributionDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(dataSource);
bean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/joylee/distribution/mapper/*.xml"));
return bean.getObject();
}
@Bean(name = "distributionDataSourceTransactionManager")
@Primary
public DataSourceTransactionManager distributionTransactionManager(@Qualifier("distributionDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "distributionSqlSessionTemplate")
@Primary
public SqlSessionTemplate distributionSqlSessionTemplate(@Qualifier("distributionSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
類似方式,建立多個Configuration以及多個Bean,因為設為@Configuration,是以在啟動的時候Bean都會建立,然後使用的時候隻需要将不同的資料庫mapper放在設定好的classpath(第35行代碼),預設走@Primary中的資料庫連接配接,如果需要走其他的,隻要在資料庫DAO中加上@@qualifier("bean名")即可。
此方案網上很多實作,這裡就是細說了。有興趣大家可以去搜尋一下spring boot mybatis多資料庫 解決方案。
此方案适用于資料庫數量固定這樣的需求,如果資料庫是動态實時修改的,那麼該怎麼處理呢,或者資料庫數量很多而且一直在擴充(如分庫場景),這樣處理顯然不行,那麼我們應該怎麼處理呢?這裡先說說解決思路。
基于Mybatis實作思路
為了了解mybatis,這裡先簡單介紹下mybatis的幾個和資料庫連接配接相關的核心類。
類 | 說明 |
---|---|
SqlSessionFactory | SqlSession的工廠,負責建立SqlSession |
SqlSession | mybatis的核心api,負責和資料庫互動的回話,該類的方法負責執行資料庫的操作 |
Configuration | mybaits的配置類 |
Environment | 資料庫環境類,主要是配置事務和資料庫連接配接 |
MappedStatement | 這兩個類負責管理具體需要執行的内和方法 |
*Handler | 主要是基于執行的方法輸入和輸出參數類型轉換處理 |
mybatis的實作代碼結構還是比較容易了解的,我們這裡重點管理資料庫連接配接的切換,所有我們重點關注的主要是:SqlSessionFactory、SqlSession、Configuration、Environment,當然還有一個datasouce類,這個類是JDBC的類,用來管理資料庫連接配接。
我最開始的思路就是就是繞過SqlSessionFactory Bean建立,因為Bean的生命周期是在程式啟動的時候執行的,看起來是無法改變的。是以我考慮了如下的方式:
針對每一個資料庫連接配接,在使用的時候去建立一個新的SqlSessionFactory,通過mybatis的api new SqlSessionFactory().build()來建立多個SqlSessionFactory,然後根據SqlSessionFactory來建立SqlSession,後面思考了一下,這是一個非常嚴重的錯誤,SqlSessionFactory本來就是用來進行SqlSession管理的,顯然建立多個SqlSessionFactory是非常不合适的,SqlSessionFactory最好是單例,SqlSessionFactory可以根據不同的資料庫建立不同的SqlSession。
基于java mybatis實作,大概的步驟就是:
//代碼來自mybaits官網
DataSource dataSource = BaseDataTest.createBlogDataSource();
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment("development", transactionFactory, dataSource);
Configuration configuration = new Configuration(environment);
configuration.setLazyLoadingEnabled(true);
configuration.setEnhancementEnabled(true);
configuration.getTypeAliasRegistry().registerAlias(Blog.class);
configuration.getTypeAliasRegistry().registerAlias(Post.class);
configuration.getTypeAliasRegistry().registerAlias(Author.class);
configuration.addMapper(BoundBlogMapper.class);
configuration.addMapper(BoundAuthorMapper.class);
SqlSessionFactoryBuilder builder = new SqlSessionFactoryBuilder();
SqlSessionFactory factory = builder.build(configuration);
基于spring mybatis 實作思路
現在的java項目幾乎都是基于spring進行開發,對于這樣的需求,肯定是可以以spring的方式進行解決的,我陷入了一個誤區,一直收到bean在建立後很難修改這個思路的影響,放棄spring bean管理的方式,但是其實這也是一個錯誤的方向。
spring bean的确是程式啟動的時候就完成了bean的建立,但是每個bean本身是有提供很多方法和屬性的,其實bean的很多屬性是可以修改的,SqlSessionFactorybean肯定會有這樣的屬性,果然,我們隻要擷取到程式啟動時建立的bean,然後修改屬性的值就可以了。
/**
* @param corpDatabaseBO 資料庫實體
* @return SqlSessionFactory
* @throws PropertyVetoException
*/
public SqlSessionFactory changeSqlSessionFactory(CorpDatabaseBO corpDatabaseBO) throws Exception {
//擷取目前SqlSessionFactory bean
SqlSessionFactory bean = SpringUtils.getBean(SqlSessionFactory.class);
if(bean==null){
throw new NullPointerException("default SqlSessionFactory bean is not created");
}
logger.info(String.format("目前的sqlsessionfactory為:%s",bean.getConfiguration().getEnvironment().getDataSource().getConnection().getCatalog()));
//因為業務需要,我用的是sqlserver
SQLServerDataSource sqlServerDataSource = new SQLServerDataSource();
sqlServerDataSource.setServerName(corpDatabaseBO.getUrl());
sqlServerDataSource.setDatabaseName(corpDatabaseBO.getDatabasename());
sqlServerDataSource.setUser(corpDatabaseBO.getUsername());
sqlServerDataSource.setPassword(corpDatabaseBO.getPassword());
sqlServerDataSource.setPortNumber(corpDatabaseBO.getPort());
//資料庫連接配接池用的是Hikari
HikariDataSource hikariDataSource = new HikariDataSource();
hikariDataSource.setDataSource(sqlServerDataSource);
TransactionFactory transactionFactory = new JdbcTransactionFactory();
Environment environment = new Environment(corpDatabaseBO.getDatabasename(), transactionFactory, hikariDataSource);
// 修改environment,這樣就可以修改資料庫位址了。
bean.getConfiguration().setEnvironment(environment);
return bean;
}
在第30行:
bean.getConfiguration().setEnvironment(environment);
通過bean擷取configuration,然後再設定Environment 即可修改資料庫連接配接。每次需要執行資料庫切換的時候,隻要重新調用changeSqlSessionFactory方法即可。
總結
- 開發中遇到問題,在自己沒有很好的解決思路的情況下,可以去網上查找相關的資料,但是有時候,因為自己業務的特殊性,其實網上是沒有很好的解決方案的,甚至網上的一些解決方案并非一個很好的解決方案,我們參考别人方案的時候,一定要弄清楚代碼每一層的意思,是否是自己真正需要的,而不是簡單的copy。
- 當網上也沒有解決方案的時候,建議多花點時間去閱讀源碼,因為這個功能,雖然不至于閱讀了mybatis所有的源碼,但是對mybatis整體的結構有了一個清晰的認識,大部分源碼也有閱讀,這算是此次最大的收獲。