SpringBoot x MyBatis でDBのReader/Writerの接続設定

エキサイト株式会社エンジニアの佐々木です。SpringBoot x MyBatis でDBのReader/Writerの接続設定をご紹介します。

application.ymlの設定

application.ymlを下記のように

spring:
  datasource:
    writer:
      username: aaa
      password: bbb
      hikari:
        max-lifetime: 600000
        maximum-pool-size: 5
        connection-test-query: "SELECT 1;"
        keep-alive-time: 60000
    reader:
      username: aaa
      password: bbb
      hikari:
        max-lifetime: 600000
        maximum-pool-size: 10
        connection-test-query: "SELECT 1;"
        keep-alive-time: 60000

Reader/Writerの設定

Reader/Writerの設定は下記のようにコードを書きます。他DBに接続するときにも使えるの汎用的なコードになります。@Beanに名前をつけ、@Qualifyで取り出しながらDataSourceの設定を行います。


// DataSourceWriterConfig.java 

@Configuration
public class DataSourceWriterConfig {

    @Bean("hikariWriter")
    @ConfigurationProperties(prefix = "spring.datasource.writer.hikari") // spring.datasource.writer.hikariのHikariCP読み込みを設定
    public HikariConfig hikariConfig() {
        return new HikariConfig();
    }

    @Bean(name = "dbWriterProperties")
    @ConfigurationProperties(prefix = "spring.datasource.writer") // spring.datasource.writer の読み込み設定
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean(name = "dbWriter")
    @DependsOn("dbWriterProperties")
    public DataSource dataSource(
            @Qualifier("hikariWriter") HikariConfig hikariConfig,
            @Qualifier("dbWriterProperties") DataSourceProperties dataSourceProperties) {
        hikariConfig.setDataSource(
                dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build()
        );
        return new HikariDataSource(hikariConfig);
    }
}


// DataSourceReaderConfig.java 

@Configuration
public class DataSourceReaderConfig {

    @Bean("hikariReader")
    @ConfigurationProperties(prefix = "spring.datasource.reader.hikari") // spring.datasource.reader.hikariのHikariCP読み込みを設定
    public HikariConfig hikariConfig() {
        return new HikariConfig();
    }

    @Bean(name = "dbReaderProperties")
    @ConfigurationProperties(prefix = "spring.datasource.reader") // spring.datasource.reader の読み込みを設定
    public DataSourceProperties dataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean(name = "dbReader")
    @DependsOn("dbReaderProperties")
    public DataSource dataSource(
            @Qualifier("hikariReader") HikariConfig hikariConfig,
            @Qualifier("dbReaderProperties") DataSourceProperties dataSourceProperties) {
        hikariConfig.setDataSource(
                dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build()
        );
        return new HikariDataSource(hikariConfig);
    }

トランザクションまわりの全体コード

下記がDBの接続設定を処理するときのトランザクションの設定になります。複数DBに接続する必要があるときは、この設定が増えていきます。(参照用、書き込み用、集計用など...)

// DataSourceTransactionConfig.java

@Configuration
@RequiredArgsConstructor
@MapperScan(basePackages = "jp.co.excite.sample.sql")
public class DataSourceTransactionConfig {
    
    private final MybatisAutoConfiguration mybatisAutoConfiguration;

    @Bean("replicationResolver")
    public ReplicationResolver routingDataSource(@Qualifier(DataSourceWriterConfig.DATASOURCE_NAME) DataSource dataSourceWriter,
                                                 @Qualifier(DataSourceReaderConfig.DATASOURCE_NAME) DataSource dataSourceReader) {

        ReplicationResolver replicationDataSource = new ReplicationResolver();
        replicationDataSource.setTargetDataSources(Map.of(DataSourceType.READER, dataSourceReader, DataSourceType.WRITER, dataSourceWriter));
        replicationDataSource.setDefaultTargetDataSource(dataSourceReader);
        return replicationDataSource;
    }

    @Bean("mainTransactionManager")
    @Primary
    public PlatformTransactionManager transactionManager(@Qualifier("readerDataSource") DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

    @Bean("mainDataSource")
    @Primary
    public DataSource dataSource(@Qualifier("replicationResolver") ReplicationResolver dataSource) {
        return new LazyConnectionDataSourceProxy(dataSource);
    }

    @Bean
    public SqlSessionFactory sessionFactory(@Qualifier("mainDataSource") DataSource dataSource) throws Exception {
        return mybatisAutoConfiguration.sqlSessionFactory(dataSource);
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    static class ReplicationResolver extends AbstractRoutingDataSource {

        @Override
        protected DataSourceType determineCurrentLookupKey() {
            return TransactionSynchronizationManager
                    .isCurrentTransactionReadOnly() ? DataSourceType.READER : DataSourceType.WRITER;
        }
    }

    static enum DataSourceType {
        WRITER,
        READER
    }
}

一つ一つずつみていきます。

ReplicationResolver, DataSourceType

下記コードをから、トランザクションの設定を行います。

    static class ReplicationResolver extends AbstractRoutingDataSource {

        @Override
        protected DataSourceType determineCurrentLookupKey() {
            return TransactionSynchronizationManager
                    .isCurrentTransactionReadOnly() ? DataSourceType.READER : DataSourceType.WRITER;
        }
    }

    static enum DataSourceType {
        WRITER,
        READER
    }

やっていることは簡単で、DataSourceType は、Enumを利用し、書き込みと読み込みを区別する目印を設定します。ReplicationResolverは、AbstractRoutingDataSourceを継承して、Routingの条件設定などを行います。シャードキーやリードオンリーのようなせていが入っているので、シンプルな要件だと実装は簡単だと思います。今回は、isCurrentTransactionReadOnly() のときに、参照用DBか更新用DBかの判定いれるために上記のように設定します。

ReplicationResolverのBean化

先ほど宣言したReplicationResolverを、Bean化します。今回は、ReaderDBとWriterDBに振り分けますので、その設定も行います。

    @Bean("replicationResolver")
    public ReplicationResolver routingDataSource(@Qualifier("dbWriter") DataSource dataSourceWriter,
                                                 @Qualifier("dbReader") DataSource dataSourceReader) {

        ReplicationResolver replicationDataSource = new ReplicationResolver();
        replicationDataSource.setTargetDataSources(Map.of(
                    DataSourceType.READER, dataSourceReader, 
                    DataSourceType.WRITER, dataSourceWriter
        ));
        replicationDataSource.setDefaultTargetDataSource(dataSourceReader);
        return replicationDataSource;
    }

setTargetDataSources()に、振り分けたいデータソースを設定します。デフォルトのデータソースも設定をします。

DataSource

Reader/WriterのときにもDataSourceは宣言したが、これはトランザクション用のDataSourceになります。ReplicationResolverでセットしたものから生成します。@Primaryをつけているのは、複数のDataSource定義があると起動時にエラーになりますので、デフォルトで接続する先を指定するためにトランザクション設定がされているものに@Primaryをつけておきます。

    @Bean("mainDataSource")
    @Primary
    public DataSource dataSource(@Qualifier("replicationResolver") ReplicationResolver replicationResolver) {
        return new LazyConnectionDataSourceProxy(replicationResolver);
    }

PlatformTransactionManager

トランザクション管理するAPIを提供するインターフェースで、今回はMyBatisを使うので、DataSourceTransactionManagerクラスを使用します。今回は1つしか設定しませんので、@Primaryは不要ですが今後のことを考えてつけておきます。

    @Bean("mainTransactionManager")
    @Primary
    public PlatformTransactionManager transactionManager(@Qualifier(DATASOURCE_MAIN) DataSource dataSource) {
        return new DataSourceTransactionManager(dataSource);
    }

複数のTransactionManagerの設定がある場合は、利用時に@Transactionalの引数で@Transactional( transactionManager = "mainTransactionManager")のような形で明示的に呼び出しが可能です。指定がない場合は、@Primaryが設定されているTransactionManagerが使用されます。

SqlSessionFactory

application.ymlからMyBatisの設定を読み込み、そこからSqlSessionFactoryDataSourceをつなぎ設定を行います。

    @Bean
    public SqlSessionFactory sessionFactory(@Qualifier("mainDataSource") DataSource dataSource) throws Exception {
        return mybatisAutoConfiguration.sqlSessionFactory(dataSource);
    }

SqlSessionTemplate

上記で作成した、SqlSessionFactoryをSqlSessionTemplateのコンストラクタに渡しBean化します。

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

まとめ

DataSourceが合計3つでてくるので、最初は混乱すると思いますが、Bean名をつけて区別しながら設定すればうまくいくと思います。AOPで切り替える方法もネットには転がっていますが、ブラックボックスになりがちなので、公式に提供されているものをなるべく駆使しております。

ご参考になればと思います。