JavaのSpringBootでMyBatisを利用して複数のデータソースに接続する方法

こんにちは、エキサイト株式会社の平石です。エキサイトホールディングス Advent Calendar 2023の5日目を担当いたします。

今回はJavaのSpringBootでMyBatisを利用して、複数のデータソースに接続する方法をご紹介します。

はじめに

複数のデータベースに利用したい情報が分散しており、一つの「アプリケーション」からそれらにアクセスする必要があるという場合を考えます。

このとき、考えられる対処法は主に2つあります。

  • それぞれのデータソースからデータを取得するためのAPIを別プロジェクトとして実装する
  • 複数のデータソースに一つのアプリケーションから直接アクセスできるような設定をする

1つ目の方法では、一つの「アプリケーション」から直接アクセスするデータソースを最大1つにすることができるので、複雑さの観点からは望ましいと言えます。 しかし、データベースに複数回アクセスする必要があるような状況では、呼び出しのオーバーヘッドが大きくなってしまいます。 また、単純にAPIを実装する手間もかかります。

そのような場合には、2つ目の方法も視野に入れることが考えられます。

本ブログでは、この2つ目の方法をご紹介いたします。

前提条件

本ブログでは以下の項目を前提条件としています。

  • Java、SpringBoot、MyBatisについてはある程度知っている
  • アプリケーションコードを記述する環境が整っている
  • 単一のデータソースにアクセスしてデータを取得することが可能なコードは実装している

動作確認環境

  • Java 17
  • SpringBoot v3.1.5
  • MyBatis ・・・ org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.2

問題設定

本ブログの例では、以下のような設定を用います。

データソース1 データソース2
DBの種類 MySQL 8.2 MySQL 5.7
ポート 3307 3308
接続先スキーマ schema1 schema2

今回はどちらもMySQLですが、当然ながらPostgreSQLなどの他のデータベースでも問題ありません。

application.ymlファイルへの記述

まずは、各データソースへの接続情報をapplication.ymlファイルに記述していきます。

spring:
  datasource:
    schema1:
      connection-properties:
        jdbc-url: jdbc:mysql://localhost:3307/schema1
        username: sample1-user
        password: sample1-password
      hikari-data-source-properties:
        max-lifetime: 600000
        maximum-pool-size: 5
    schema2:
      connection-properties:
        jdbc-url: jdbc:mysql://localhost:3308/schema2
        username: sample2-user
        password: sample2-password
      hikari-data-source-properties:
        max-lifetime: 600000
        maximum-pool-size: 5

もちろん、コード内に設定情報を記述しても動きます。 しかし、これらのパラメータはプロパティファイルにまとめておいた方が良いでしょう。

hikari-data-source-properties内のmax-lifetimemaximum-pool-sizeは今回の要件では必須ではないですが、カスタマイズできるようにしておくと後々便利です。

なお、キーはこの通りでなくても問題ありませんので、それぞれのデータソースに合わせて適切なキー名にしてください。 また、usernamepasswordも適宜変更してください。

接続設定項目をYAMLファイルから読み込む

次に、前の節で記述したYAMLファイルの内容を読み込むためのコードを記述します。

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "spring.datasource")
public record CustomDataSourceProperties(Properties schema1, Properties schema2) {
    public record Properties(
            ConnectionProperties connectionProperties, HikariDataSourceProperties hikariDataSourceProperties
    ) {
        public record ConnectionProperties(String jdbcUrl, String username, String password) {}

        public record HikariDataSourceProperties(
                Integer maxLifetime,
                Integer minimumIdle,
                Integer maximumPoolSize
        ) {
        }
    }
}

イメージとしては、@ConfigurationPropertiesprefixで指定したspring.datasourceの配下に、connectionPropertieshikariDataSourcePropertiesを配下にもつPropertiesレコードであるschema1schema2があるという設定になっています。

YAMLファイルのキーと見比べてみると、なんとなく記述していることは見えてくるのではないでしょうか。

実際に接続設定を行う

それでは、実際に接続のための設定を記述していきましょう。

といっても、SpringBootでMyBatisをデータベースに接続する際に、接続設定をカスタマイズするのとほとんど変わりません。

import javax.sql.DataSource;
import java.util.Objects;

import com.example.sample_project.project1.property.CustomDataSourceProperties;
import com.zaxxer.hikari.HikariDataSource;
import lombok.RequiredArgsConstructor;
import org.apache.ibatis.scripting.defaults.RawLanguageDriver;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
@MapperScan(
        basePackages = "com.example.sample_project.persistence.schema1",
        sqlSessionFactoryRef = "schema1SqlSessionFactory"
)
@RequiredArgsConstructor
public class Schema1DataSourceConfig {
    private static final String SCHEMA1_DATA_SOURCE_NAME = "schema1DataSource";
    private static final String SCHEMA1_DATA_SOURCE_PROPERTIES_NAME = "schema1DataSourceProperties";
    private static final String SCHEMA1_SQL_SESSION_FACTORY_NAME = "schema1SqlSessionFactory";

    private static final String DRIVER_CLASS_NAME = "com.mysql.cj.jdbc.Driver";

    private final CustomDataSourceProperties customDataSourceProperties;

    /**
     * schema1スキーマ用のDataSourcePropertiesを取得
     *
     * @return schema1スキーマ用のDataSourceProperties
     */
    public DataSourceProperties getSchema1DataSourceProperties() {
        return mapToDataSourceProperties(
                customDataSourceProperties.schema1().connectionProperties().username(),
                customDataSourceProperties.schema1().connectionProperties().password(),
                customDataSourceProperties.schema1().connectionProperties().jdbcUrl()
        );
    }

    /**
     * DataSourcePropertiesを取得  ①
     *
     * @param url DatabaseのエンドポイントURL
     * @return DataSourceProperties
     */
    private DataSourceProperties mapToDataSourceProperties(String username, String password, String url) {
        DataSourceProperties dataSourceProperties = new DataSourceProperties();
        dataSourceProperties.setDriverClassName(DRIVER_CLASS_NAME);
        dataSourceProperties.setUsername(username);
        dataSourceProperties.setPassword(password);
        dataSourceProperties.setUrl(url);

        return dataSourceProperties;
    }

    /**
     * schema1スキーマ用のDataSourceのプロパティを作成
     *
     * @return DataSourceのプロパティ
     */
    @Bean(name = SCHEMA1_DATA_SOURCE_PROPERTIES_NAME)
    @Primary
    public DataSourceProperties schema1DataSourceProperties() {
        return getSchema1DataSourceProperties();
    }

    /**
     * schema1スキーマ用のDataSourceを作成
     *
     * @return DataSource
     */
    @Bean(name = SCHEMA1_DATA_SOURCE_NAME)
    @Primary
    public DataSource schema1DataSource(@Qualifier(SCHEMA1_DATA_SOURCE_PROPERTIES_NAME) DataSourceProperties dataSourceProperties) {
        final HikariDataSource hikariDataSource = dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
        return this.setPropertiesToHikariDataSource(hikariDataSource, customDataSourceProperties.schema1().hikariDataSourceProperties());
    }

    /**
     * HikariDataSourceにプロパティをセットする  ②
     *
     * @param hikariDataSource セット先HikariDataSource
     * @param properties セットしたいプロパティ
     * @return プロパティをセットしたHikariDataSource
     */
    private HikariDataSource setPropertiesToHikariDataSource(
            HikariDataSource hikariDataSource,
            CustomDataSourceProperties.Properties.HikariDataSourceProperties properties
    ) {
        hikariDataSource.setMaxLifetime(properties.maxLifetime());
        hikariDataSource.setMaximumPoolSize(properties.maximumPoolSize());

        if (Objects.nonNull(properties.minimumIdle())) {
            hikariDataSource.setMinimumIdle(properties.minimumIdle());
        }

        return hikariDataSource;
    }

    /**
     * schema1スキーマ用のSQLセッションファクトリを作成
     *
     * @return SQLセッションファクトリ
     * @throws Exception 例外
     */
    @Bean(name = SCHEMA1_SQL_SESSION_FACTORY_NAME)
    @Primary
    public SqlSessionFactory schema1SqlSessionFactory(@Qualifier(SCHEMA1_DATA_SOURCE_NAME) DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);

        final SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBean.getObject();
        if (Objects.isNull(sqlSessionFactory)) {
            throw new NullPointerException();
        }

        // この辺りの設定は適宜変更してください。
        sqlSessionFactory.getConfiguration().setCacheEnabled(false);
        sqlSessionFactory.getConfiguration().setMapUnderscoreToCamelCase(true);
        sqlSessionFactory.getConfiguration().setDefaultScriptingLanguage(RawLanguageDriver.class);

        return sqlSessionFactory;
    }
}

①で、データベースの接続設定をDataSourcePropertiesにセットしています。
また、②で接続の最大ライフタイムや、最大プールサイズのような設定をHikariDataSourceへセットしています。

この辺りは、接続先が1つの場合にも設定する項目です。

大きな違いはこの設定を接続先のデータベースの数だけ記述する必要があることです。 先ほどの設定は「データベース1」の「schema1」用の接続設定です。
よって、今度は「データベース2」の「schema2」用の接続設定を記述していきましょう。

import javax.sql.DataSource;
import java.util.Objects;

import com.example.sample_project.project1.property.CustomDataSourceProperties;
import com.zaxxer.hikari.HikariDataSource;
import lombok.RequiredArgsConstructor;
import org.apache.ibatis.scripting.defaults.RawLanguageDriver;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
@MapperScan(
        basePackages = "com.example.sample_project.persistence.schema2",
        sqlSessionFactoryRef = "schema2SqlSessionFactory"
)
@RequiredArgsConstructor
public class Schema2DataSourceConfig {
    private static final String SCHEMA2_DATA_SOURCE_NAME = "schema2DataSource";
    private static final String SCHEMA2_DATA_SOURCE_PROPERTIES_NAME = "schema2DataSourceProperties";
    private static final String SCHEMA2_SQL_SESSION_FACTORY_NAME = "schema2SqlSessionFactory";

    private static final String DRIVER_CLASS_NAME = "com.mysql.cj.jdbc.Driver";

    private final CustomDataSourceProperties customDataSourceProperties;

    /**
     * schema2スキーマ用のDataSourcePropertiesを取得
     *
     * @return schema2スキーマ用のDataSourceProperties
     */
    public DataSourceProperties getSchema2DataSourceProperties() {
        return mapToDataSourceProperties(
                customDataSourceProperties.schema2().connectionProperties().username(),
                customDataSourceProperties.schema2().connectionProperties().password(),
                customDataSourceProperties.schema2().connectionProperties().jdbcUrl()
        );
    }

    /**
     * DataSourcePropertiesを取得
     *
     * @param url DatabaseのエンドポイントURL
     * @return DataSourceProperties
     */
    private DataSourceProperties mapToDataSourceProperties(String username, String password, String url) {
        DataSourceProperties dataSourceProperties = new DataSourceProperties();
        dataSourceProperties.setDriverClassName(DRIVER_CLASS_NAME);
        dataSourceProperties.setUsername(username);
        dataSourceProperties.setPassword(password);
        dataSourceProperties.setUrl(url);

        return dataSourceProperties;
    }

    /**
     * schema2スキーマ用のDataSourceのプロパティを作成
     *
     * @return DataSourceのプロパティ
     */
    @Bean(name = SCHEMA2_DATA_SOURCE_PROPERTIES_NAME)
    @Primary
    public DataSourceProperties schema2DataSourceProperties() {
        return getSchema2DataSourceProperties();
    }

    /**
     * schema2スキーマ用のDataSourceを作成
     *
     * @return DataSource
     */
    @Bean(name = SCHEMA2_DATA_SOURCE_NAME)
    @Primary
    public DataSource schema2DataSource(@Qualifier(SCHEMA2_DATA_SOURCE_PROPERTIES_NAME) DataSourceProperties dataSourceProperties) {
        return dataSourceProperties.initializeDataSourceBuilder().type(HikariDataSource.class).build();
    }

    /**
     * schema2スキーマ用のSQLセッションファクトリを作成
     *
     * @return SQLセッションファクトリ
     * @throws Exception 例外
     */
    @Bean(name = SCHEMA2_SQL_SESSION_FACTORY_NAME)
    @Primary
    public SqlSessionFactory schema2SqlSessionFactory(@Qualifier(SCHEMA2_DATA_SOURCE_NAME) DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);

        final SqlSessionFactory sqlSessionFactory = sqlSessionFactoryBean.getObject();
        if (Objects.isNull(sqlSessionFactory)) {
            throw new NullPointerException();
        }

        sqlSessionFactory.getConfiguration().setCacheEnabled(false);
        sqlSessionFactory.getConfiguration().setMapUnderscoreToCamelCase(true);
        sqlSessionFactory.getConfiguration().setDefaultScriptingLanguage(RawLanguageDriver.class);

        return sqlSessionFactory;
    }
}

コードの構成自体は「データベース1」の「schema1」への設定と全く同じですが、

customDataSourceProperties.schema2().connectionProperties().username()

のように所々、「schema2」用の設定を読み込んでいます。

注意していただきたいのは、@Beanアノテーションを付けて各データソースについて登録したBeanは型が重複してしまうことです。
そこで、nameパラメータを指定し、DIする際にはQualifierを付けてBeanを指定しています。

このコードで注目してほしい点は、 各データソースへの接続設定クラスにつけている以下のアノテーションです。

@MapperScan(
        basePackages = "com.example.sample_project.persistence.schema1",
        sqlSessionFactoryRef = "schema1SqlSessionFactory"
)
@MapperScan(
        basePackages = "com.example.sample_project.persistence.schema2",
        sqlSessionFactoryRef = "schema2SqlSessionFactory"
)

これを、設定することで、com.example.sample_project.persistence.schema1パッケージに記述したクエリはschema1SqlSessionFactoryを使って、「データベース1」の「schema1」へ接続して実行され、

com.example.sample_project.persistence.schema2パッケージに記述したクエリはschema2SqlSessionFactoryを使って、「データベース2」の「schema2」へ接続して実行されるという設定を行うことができるのです。

つまり、それぞれのデータソースで実行されるクエリを記述するためのパッケージを分けることができ、まとまりが良くなります。

なお、指定するパッケージはご自身の使用したいものを設定してください。

これで、複数のデータソースに接続するための設定はおわりです。

2度目ですが、この設定の場合には、「データベース1」の「schema1」からデータを取得する場合には、com.example.sample_project.persistence.schema1パッケージに、「データベース2」の「schema2」からデータを取得する場合には、com.example.sample_project.persistence.schema2パッケージにクエリを記述したコード or ファイルを置くようにしてください。

おわりに

今回は、JavaのSpringBootでMyBatisを利用して複数のデータソースに接続する方法を紹介しました。

では、また次回。

参考文献