Spring BootでDataSourceのBeanを複数作る
この記事について
Spring Bootでは、Auto Configurationにより DataSource
のBeanが1つ作られます。しかし自分で明示的に DataSource
のBeanを定義すると、Auto Configurationによって作られるはずだった DataSource
Beanが作られなくなってしまいます。
では、どうやって DataSource
のBeanを2つ作るかと言うと、自分で明示的に DataSource
のBeanを2つ作ればよいのです。
方法は大きく2つです。
- 方法①
DataSource
などのサブクラスを各DB用に作る - 方法②
@Qualifier
を利用する
方法①のみ、ソースコードはGitHubにおいてあります。
環境
- JDK 17
- Spring Boot 2.7.0
- spring-boot-starter-web
- spring-boot-starter-jdbc
- PostgreSQL 14.3 (Docker Composeで2つ起動)
- Docker Compose v2.5.1
多少バージョンが違っていても同様に動くと思います。
方法①・②共通してやること
application.yamlに設定を記述
DataSource
がAuto Confogurationで1つだけ作られる通常の場合、 spring.datasource
で始まるプロパティを記述しますね。しかし、 DataSource
が2つの場合はこのプロパティは効果がありません。
なので、独自のプロパティを定義します。
db01:
datasource:
poolName: Pool01
jdbcUrl: jdbc:postgresql://localhost:5001/postgres01
username: postgres01
password: postgres01
driverClassName: org.postgresql.Driver
autoCommit: false
connectionTimeout: 500
jdbctemplate:
queryTimeout: 1
maxRows: 10
fetchSize: 10
db02:
datasource:
poolName: Pool02
jdbcUrl: jdbc:postgresql://localhost:5002/postgres02
username: postgres02
password: postgres02
driverClassName: org.postgresql.Driver
autoCommit: false
connectionTimeout: 500
jdbctemplate:
queryTimeout: 1
maxRows: 10
fetchSize: 10
db01
・ db02
・ datasource
・ jdbctemplate
属性については、後述する @ConfigurationProperties
で指定します(つまり、指定と一致すればどんな名前でもいい)。
db01.datasource
・ db02.datasource
配下の poolName
・ jdbcUrl
などの名前は、 HikariConfig
クラスのsetterメソッド名と揃えてください(例: setPoolName()
-> poolName
)。
db01.jdbctemplate
・ db02.jdbctemplate
配下の queryTimeout
・ maxRows
などの名前は、 JdbcTemplate
クラスのsetterメソッド名と揃えてください(例: setQueryTimeout()
-> queryTimeout
)。
各DB用のカスタム@Transationalアノテーションを作成する
@Transactional
には transactionManager
要素があり、これにトランザクションマネージャー名を指定します。
transactionManager
要素に何も指定しない場合は、DIコンテナ内に1つだけあるトランザクションマネージャーが使われます。
@Transactional
を使う全箇所でトランザクションマネージャー名を指定すると後から修正が大変になるので、指定済みのカスタムアノテーションを作成するとよいでしょう。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Transactional(transactionManager = Db01Config.DB01_TRANSACTION_MANAGER_NAME)
public @interface Db01Transactional {
...
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Transactional(transactionManager = Db02Config.DB02_TRANSACTION_MANAGER_NAME)
public @interface Db02Transactional {
...
}
DataSource
などのサブクラスを各DB用に作る
[推奨] 方法① サブクラスの作成
DataSource
やトランザクションマネージャーなどのサブクラスを、各DB用に作ります。
具体的には、次のクラスたちのサブクラスを作ります。
HikariConfig
HikariDataSource
JdbcTemplate
JdbcTransactionManager
public class Db01HikariConfig extends HikariConfig {
public Db01HikariConfig() {
}
}
public class Db02HikariConfig extends HikariConfig {
public Db02HikariConfig() {
}
}
public class Db01DataSource extends HikariDataSource {
// HikariConfigではなくDb01HikariConfigをDIしているのがポイント
public Db01DataSource(Db01HikariConfig hikariConfig) {
super(hikariConfig);
}
}
public class Db02DataSource extends HikariDataSource {
// HikariConfigではなくDb02HikariConfigをDIしているのがポイント
public Db02DataSource(Db02HikariConfig hikariConfig) {
super(hikariConfig);
}
}
public class Db01JdbcTemplate extends JdbcTemplate {
// DataSourceではなくDb01DataSourceをDIしているのがポイント
public Db01JdbcTemplate(Db01DataSource dataSource) {
super(dataSource);
}
}
public class Db02JdbcTemplate extends JdbcTemplate {
// DataSourceではなくDb02DataSourceをDIしているのがポイント
public Db02JdbcTemplate(Db02DataSource dataSource) {
super(dataSource);
}
}
public class Db01TransactionManager extends JdbcTransactionManager {
// DataSourceではなくDb01DataSourceをDIしているのがポイント
public Db01TransactionManager(Db01DataSource dataSource) {
super(dataSource);
}
}
public class Db02TransactionManager extends JdbcTransactionManager {
// DataSourceではなくDb02DataSourceをDIしているのがポイント
public Db02ransactionManager(Db01DataSource dataSource) {
super(dataSource);
}
}
Bean定義
作成したサブクラスたちをBean定義していきます。
@ConfigurationProperties
を付加することで、 prefix
で指定したプロパティが適用されるようにします。
@Configuration
public class Db01Config {
public static final String DB01_TRANSACTION_MANAGER_NAME = "db01TransactionManager";
@Bean
@ConfigurationProperties(prefix = "db01.datasource")
public Db01HikariConfig db01HikariConfig() {
return new Db01HikariConfig();
}
@Bean
public Db01DataSource db01DataSource(Db01HikariConfig hikariConfig) {
return new Db01DataSource(hikariConfig);
}
@Bean
@ConfigurationProperties(prefix = "db01.jdbctemplate")
public Db01JdbcTemplate db01JdbcTemplate(Db01DataSource dataSource) {
return new Db01JdbcTemplate(dataSource);
}
@Bean(name = DB01_TRANSACTION_MANAGER_NAME)
public Db01TransactionManager db01TransactionManager(Db01DataSource dataSource) {
return new Db01TransactionManager(dataSource);
}
}
@Configuration
public class Db02Config {
public static final String DB02_TRANSACTION_MANAGER_NAME = "db02TransactionManager";
@Bean
@ConfigurationProperties(prefix = "db02.datasource")
public Db02HikariConfig db02HikariConfig() {
return new Db02HikariConfig();
}
@Bean
public Db02DataSource db02DataSource(Db02HikariConfig hikariConfig) {
return new Db02DataSource(hikariConfig);
}
@Bean
@ConfigurationProperties(prefix = "db02.jdbctemplate")
public Db02JdbcTemplate db02JdbcTemplate(Db02DataSource dataSource) {
return new Db02JdbcTemplate(dataSource);
}
@Bean(name = DB02_TRANSACTION_MANAGER_NAME)
public Db02TransactionManager db02TransactionManager(Db02DataSource dataSource) {
return new Db02TransactionManager(dataSource);
}
}
リポジトリクラス
@Repository
public class Db01SampleRepository {
private final Db01JdbcTemplate jdbcTemplate;
// JdbcTemplateではなくDb01JdbcTemplateをDIしているのがポイント
public Db01SampleRepository(Db01JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
...
}
@Repository
public class Db02SampleRepository {
private final Db02JdbcTemplate jdbcTemplate;
// JdbcTemplateではなくDb02JdbcTemplateをDIしているのがポイント
public Db02SampleRepository(Db02JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
...
}
サービスクラス
@Service
public class Db01SampleService {
private final Db01SampleRepository sampleRepository;
public Db01SampleService(Db01SampleRepository sampleRepository) {
this.sampleRepository = sampleRepository;
}
@Db01Transactional(readOnly = true)
public List<Sample> findAll() {
...
}
}
@Service
public class Db02SampleService {
private final Db02SampleRepository sampleRepository;
public Db02SampleService(Db02SampleRepository sampleRepository) {
this.sampleRepository = sampleRepository;
}
@Db02Transactional(readOnly = true)
public List<Sample> findAll() {
...
}
}
なぜこの方法がオススメなのか
理由は2つあります。
- 型でDIするので間違いにくくなる
- トランザクションマネージャーのロガー名が各DBで変わる(下記参照)
20xx-xx-xx xx:xx:xx.xxx DEBUG 63568 --- [nio-8080-exec-1] c.e.s.db01.Db01TransactionManager : Creating new transaction with name [com.example.springbootmultidatasource.db01.Db01SampleService.findAll]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly; 'db01TransactionManager'
20xx-xx-xx xx:xx:xx.xxx DEBUG 63568 --- [nio-8080-exec-1] c.e.s.db01.Db01TransactionManager : Acquired Connection [HikariProxyConnection@582968058 wrapping org.postgresql.jdbc.PgConnection@355c94be] for JDBC transaction
20xx-xx-xx xx:xx:xx.xxx DEBUG 63568 --- [nio-8080-exec-1] c.e.s.db01.Db01JdbcTemplate : Executing SQL query [SELECT id, content FROM sample01 ORDER BY id
]
20xx-xx-xx xx:xx:xx.xxx DEBUG 63568 --- [nio-8080-exec-1] c.e.s.db01.Db01TransactionManager : Initiating transaction commit
20xx-xx-xx xx:xx:xx.xxx DEBUG 63568 --- [nio-8080-exec-1] c.e.s.db01.Db01TransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@582968058 wrapping org.postgresql.jdbc.PgConnection@355c94be]
20xx-xx-xx xx:xx:xx.xxx DEBUG 63568 --- [nio-8080-exec-1] c.e.s.db01.Db01TransactionManager : Releasing JDBC Connection [HikariProxyConnection@582968058 wrapping org.postgresql.jdbc.PgConnection@355c94be] after transaction
20xx-xx-xx xx:xx:xx.xxx DEBUG 63568 --- [nio-8080-exec-1] c.e.s.db02.Db02TransactionManager : Creating new transaction with name [com.example.springbootmultidatasource.db02.Db02SampleService.findAll]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly; 'db02TransactionManager'
20xx-xx-xx xx:xx:xx.xxx DEBUG 63568 --- [nio-8080-exec-1] c.e.s.db02.Db02TransactionManager : Acquired Connection [HikariProxyConnection@1714331332 wrapping org.postgresql.jdbc.PgConnection@7e307087] for JDBC transaction
20xx-xx-xx xx:xx:xx.xxx DEBUG 63568 --- [nio-8080-exec-1] c.e.s.db02.Db02JdbcTemplate : Executing SQL query [SELECT id, content FROM sample02 ORDER BY id
]
20xx-xx-xx xx:xx:xx.xxx DEBUG 63568 --- [nio-8080-exec-1] c.e.s.db02.Db02TransactionManager : Initiating transaction commit
20xx-xx-xx xx:xx:xx.xxx DEBUG 63568 --- [nio-8080-exec-1] c.e.s.db02.Db02TransactionManager : Committing JDBC transaction on Connection [HikariProxyConnection@1714331332 wrapping org.postgresql.jdbc.PgConnection@7e307087]
20xx-xx-xx xx:xx:xx.xxx DEBUG 63568 --- [nio-8080-exec-1] c.e.s.db02.Db02TransactionManager : Releasing JDBC Connection [HikariProxyConnection@1714331332 wrapping org.postgresql.jdbc.PgConnection@7e307087] after transaction
このような目的で継承を使うのは違和感があるかもしれませんが、どのログがどのDBへのトランザクションなのかを瞬時に区別できるメリットは大きいです。
@Qualifier
を利用する
方法② 「DataSourceが複数ある場合にSpring Testの@Sqlを使う場合の落とし穴」で使っている方法です。
継承を使うのに違和感がある場合は、こちらの方法を使いましょう。
カスタム@Qualifierアノテーションを作成する
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD})
@Qualifier
public @interface Db01 {
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD})
@Qualifier
public @interface Db02 {
}
Bean定義
Beanを定義している箇所、およびそれらをDIしている箇所の両方に、作成したカスタム @Qualifier
アノテーションを負荷します。
@Configuration
public class Db01Config {
public static final String DB01_TRANSACTION_MANAGER_NAME = "db01TransactionManager";
@Bean
@Db01
@ConfigurationProperties(prefix = "db01.datasource")
public HikariConfig db01HikariConfig() {
return new HikariConfig();
}
@Bean
@Db01
public DataSource db01DataSource(@Db01 HikariConfig hikariConfig) {
return new DataSource(hikariConfig);
}
@Bean
@Db01
@ConfigurationProperties(prefix = "db01.jdbctemplate")
public JdbcTemplate db01JdbcTemplate(@Db01 DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean(name = DB01_TRANSACTION_MANAGER_NAME)
@Db01
public PlatformTransactionManager db01TransactionManager(@Db01 DataSource dataSource) {
return new JdbcTransactionManager(dataSource);
}
}
@Configuration
public class Db02Config {
public static final String DB02_TRANSACTION_MANAGER_NAME = "db02TransactionManager";
@Bean
@Db02
@ConfigurationProperties(prefix = "db02.datasource")
public HikariConfig db02HikariConfig() {
return new HikariConfig();
}
@Bean
@Db02
public DataSource db02DataSource(@Db02 HikariConfig hikariConfig) {
return new DataSource(hikariConfig);
}
@Bean
@Db02
@ConfigurationProperties(prefix = "db02.jdbctemplate")
public JdbcTemplate db02JdbcTemplate(@Db02 DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
@Bean(name = DB02_TRANSACTION_MANAGER_NAME)
@Db02
public PlatformTransactionManager db02TransactionManager(@Db02 DataSource dataSource) {
return new JdbcTransactionManager(dataSource);
}
}
リポジトリクラス
@Repository
public class Db01SampleRepository {
private final JdbcTemplate jdbcTemplate;
public Db01SampleRepository(@Db01 JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
...
}
@Repository
public class Db02SampleRepository {
private final JdbcTemplate jdbcTemplate;
public Db02SampleRepository(@Db02 JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
...
}
サービスクラス
方法①と同じなので省略します。
注意点
JUnitテストクラスで @Sql
を使うときに注意点があります。
詳細は👇の記事をご覧ください。
Discussion