こんにちは、エキサイト株式会社の平石です。エキサイトホールディングス 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-lifetime
やmaximum-pool-size
は今回の要件では必須ではないですが、カスタマイズできるようにしておくと後々便利です。
なお、キーはこの通りでなくても問題ありませんので、それぞれのデータソースに合わせて適切なキー名にしてください。
また、username
やpassword
も適宜変更してください。
接続設定項目を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 ) { } } }
イメージとしては、@ConfigurationProperties
のprefix
で指定したspring.datasource
の配下に、connectionProperties
とhikariDataSourceProperties
を配下にもつProperties
レコードであるschema1
とschema2
があるという設定になっています。
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を利用して複数のデータソースに接続する方法を紹介しました。
では、また次回。