SpringBootで2つのデータソースを使う(MyBatis)


概要

このエントリでは、SpringBootで、2つ(以上)のデータソースを使うにあたり、アノテーションベースのJavaConfigで実装するときの例を示します。
複数データソースの例では、JPAが使われているものを見かけますが、MyBatisなど他のマッパーの例もあると参考になる人がいるのではないか、というのがエントリ作成のきっかけです。

変更

  • 2018/5/24 @MapperScanを使うようにしました。GitHub上のコードは、「release-qiita-20180524」と対応します。

用意するもの

ツール類

  • お好きなJava開発環境
  • 下記のリポジトリにソースをおきました。(原稿執筆時点のものは"release-qiita-20180524"です。) https://github.com/hrkt/two-ds-sample
  • Docker、docker-composeコマンド(手元マシンでMySQLを2つ立ち上げるため)
  • 「mybatis-spring-boot-starter」を使っています。

知識

以下の方を前提にしています。

  • SpringBootでTutorial程度のプログラムを動かしたことがある
  • MyBatisを使ったことがあり自分でMapperを書ける
  • MySQLを起動させて、何らかのツール(MySQL Workbench等)でデータベースのテーブル内容が確認できる
  • lombokを使って書かれたコードが理解できる(@Data, @Builder, @Slf4j, @RequiredArgsConstructor, @valを使っています)

関連記事

MyBatis-Springのサイトはこちらです。

-MyBatis-Spring

Qiita内では下記の記事が関連します。

Qiitaの外では下記の記事がありました。

動かし方と観察例

  • お好きなIDEで前記リポジトリをインポートして、src/test以下のテストを動かしてください。
  • それぞれのMapperで別々のDBにアクセスする様子がTodoMapper1Test.java, TodoMapper2Test.javaで見られます。
  • サービス内で複数のデータソースのリポジトリを触る時、普通の使い方では、トランザクションマネージャが異なる方についてはトランザクションがロールバックされていないことが確認できます。(MultipleTodoServiceTest#save_list_fail)

コメント

接続設定

SpringBootのapplication.ymlで、2つの接続先を指定しています。これを後述のDataSourceConfigクラスで使っています。

データベース周りの動きた見やすいよう、
「logging.level.org.springframework.jdbc: debug」を指定して、ログを多めに出すようにしています。使われているデータソースや、トランザクションマネージャの動きが追えるようになります。


spring:
  datasource:
      datasource1:
        url: jdbc:mysql://127.0.0.1:13306/database?useSSL=false&requireSSL=false
        username: root
        password: example1
        driverClassName: com.mysql.cj.jdbc.Driver

      datasource2:
        url: jdbc:mysql://127.0.0.1:23306/database?useSSL=false&requireSSL=false
        username: root
        password: example2
        driverClassName: com.mysql.cj.jdbc.Driver

logging.level.org.springframework.jdbc: debug

データソース

データソースは、このような形で2つ用意しています。

プライマリ側

片方のデータソースには、「@Primary」をつけておきます。デフォルトで使用されるデータソースとなります。複数データソースがあるとき、一つも指定がないとエラーが出ますので、何かにつけておく必要があります。

@Configuration
@MapperScan(basePackages = {"io.hrkt.twodssample.infrastructure.ds1"},
    sqlSessionFactoryRef = "sqlSessionFactory1")
public class DataSource1Config {

  @Bean
  @Primary
  @ConfigurationProperties(prefix = "spring.datasource.datasource1")
  public DataSourceProperties datasource1Properties() {
    return new DataSourceProperties();
  }

  @Bean(name = {"datasource1"})
  @Primary
  public DataSource datasource1(
      @Qualifier("datasource1Properties") DataSourceProperties properties) {
    return properties.initializeDataSourceBuilder().build();
  }

  @Bean(name = {"txManager1"})
  @Primary
  public PlatformTransactionManager txManager1(DataSource dataSource1) {
    return new DataSourceTransactionManager(dataSource1);
  }

  @Bean(name = {"sqlSessionFactory1"})
  @Primary
  public SqlSessionFactory sqlSessionFactory(@Qualifier("datasource1") DataSource datasource1)
      throws Exception {
    SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
    sqlSessionFactory.setDataSource(datasource1);
    return (SqlSessionFactory) sqlSessionFactory.getObject();
  }
}

プライマリ側以外

プライマリ側以外には「@Primary」をつけませんが、その他の指定は同様にしてあります。

@Configuration
@MapperScan(basePackages = {"io.hrkt.twodssample.infrastructure.ds2"},
    sqlSessionFactoryRef = "sqlSessionFactory2")
public class DataSource2Config {

  @Bean
  @ConfigurationProperties(prefix = "spring.datasource.datasource2")
  public DataSourceProperties datasource2Properties() {
    return new DataSourceProperties();
  }

  @Bean(name = {"datasource2"})
  public DataSource datasource2(
      @Qualifier("datasource2Properties") DataSourceProperties properties) {
    return properties.initializeDataSourceBuilder().build();
  }

  @Bean(name = {"txManager2"})
  public PlatformTransactionManager txManager2(@Qualifier("datasource2") DataSource dataSource2) {
    return new DataSourceTransactionManager(dataSource2);
  }

  @Bean(name = {"sqlSessionFactory2"})
  public SqlSessionFactory sqlSessionFactory(@Qualifier("datasource2") DataSource datasource2)
      throws Exception {
    SqlSessionFactoryBean sqlSessionFactory = new SqlSessionFactoryBean();
    sqlSessionFactory.setDataSource(datasource2);
    return (SqlSessionFactory) sqlSessionFactory.getObject();
  }
}

ポイント

サンプルを書いていて、こうした方がよいのでは、ということで気づいた点を下記に列挙します。

  • @Beanには名前をつけてあげる。(ついていないと、一見エラーなく動いているように見えても、期待したものを使っていないことになってしまう場合などがあると思います。)

Mapperは、MapperFactoryBeanをBean定義してあげる
というのを「release-qiita-20180520」では書いていたのですが、@MapperScanで指定する方法を教えてもらったのでこちらはいらなくなりました。

Mapper

Mapperは、プライマリ側もそれ以外も指定方法に差はありません。transactionManagerは別々のものを指定しています。

@Transactional(transactionManager = "txManager1")
public interface TodoMapper1 extends TodoRepository1 {
  @Override
  @Select("SELECT todo from todos")
  public List<TodoEntry> selectList();

  @Override
  @Select("INSERT todos(todo) VALUES(#{todo})")
  public void save(TodoEntry todoEntry);
}

package io.hrkt.twodssample.infrastructure.ds2;

import java.util.List;
import org.apache.ibatis.annotations.Select;
import org.springframework.transaction.annotation.Transactional;
import io.hrkt.twodssample.domain.domain.TodoEntry;
import io.hrkt.twodssample.domain.domain2.TodoRepository2;

@Transactional(transactionManager = "txManager2")
public interface TodoMapper2 extends TodoRepository2 {
  @Override
  @Select("SELECT todo from todos")
  public List<TodoEntry> selectList();

  @Override
  @Select("INSERT todos(todo) VALUES(#{todo})")
  public void save(TodoEntry todoEntry);
}

おわりに

  • 本エントリでは、2つのデータソースを使うときの例を示しました。
  • カバレッジはこんな感じです。(lombok.configで、Jacoco0.8以降の、レポートからの除外機能を利用する設定を入れています)