天天看點

spring mybaits多資料源動态切換

場景說明

日常開發中,連接配接多個資料庫是一個很常見的需求,我們的系統是基于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整體的結構有了一個清晰的認識,大部分源碼也有閱讀,這算是此次最大的收獲。