🐥

Spring BootでDataSourceのBeanを複数作る

2024/08/13に公開

この記事について

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つの場合はこのプロパティは効果がありません。

なので、独自のプロパティを定義します。

application.yaml
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

db01db02datasourcejdbctemplate 属性については、後述する @ConfigurationProperties で指定します(つまり、指定と一致すればどんな名前でもいい)。

db01.datasourcedb02.datasource 配下の poolNamejdbcUrl などの名前は、 HikariConfig クラスのsetterメソッド名と揃えてください(例: setPoolName() -> poolName )。

db01.jdbctemplatedb02.jdbctemplate 配下の queryTimeoutmaxRows などの名前は、 JdbcTemplate クラスのsetterメソッド名と揃えてください(例: setQueryTimeout() -> queryTimeout )。

各DB用のカスタム@Transationalアノテーションを作成する

@Transactional には transactionManager 要素があり、これにトランザクションマネージャー名を指定します。

transactionManager 要素に何も指定しない場合は、DIコンテナ内に1つだけあるトランザクションマネージャーが使われます。

@Transactional を使う全箇所でトランザクションマネージャー名を指定すると後から修正が大変になるので、指定済みのカスタムアノテーションを作成するとよいでしょう。

Db01Transactional.java
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@Transactional(transactionManager = Db01Config.DB01_TRANSACTION_MANAGER_NAME)
public @interface Db01Transactional {
    ...
}
Db02Transactional.java
@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
Db01HikariConfig.java
public class Db01HikariConfig extends HikariConfig {

    public Db01HikariConfig() {
    }
}
Db02HikariConfig.java
public class Db02HikariConfig extends HikariConfig {

    public Db02HikariConfig() {
    }
}
Db01DataSource.java
public class Db01DataSource extends HikariDataSource {

    // HikariConfigではなくDb01HikariConfigをDIしているのがポイント
    public Db01DataSource(Db01HikariConfig hikariConfig) {
        super(hikariConfig);
    }
}
Db02DataSource.java
public class Db02DataSource extends HikariDataSource {

    // HikariConfigではなくDb02HikariConfigをDIしているのがポイント
    public Db02DataSource(Db02HikariConfig hikariConfig) {
        super(hikariConfig);
    }
}
Db01JdbcTemplate.java
public class Db01JdbcTemplate extends JdbcTemplate {

    // DataSourceではなくDb01DataSourceをDIしているのがポイント
    public Db01JdbcTemplate(Db01DataSource dataSource) {
        super(dataSource);
    }
}
Db02JdbcTemplate.java
public class Db02JdbcTemplate extends JdbcTemplate {

    // DataSourceではなくDb02DataSourceをDIしているのがポイント
    public Db02JdbcTemplate(Db02DataSource dataSource) {
        super(dataSource);
    }
}
Db01TransactionManager.java
public class Db01TransactionManager extends JdbcTransactionManager {

    // DataSourceではなくDb01DataSourceをDIしているのがポイント
    public Db01TransactionManager(Db01DataSource dataSource) {
        super(dataSource);
    }
}
Db02TransactionManager.java
public class Db02TransactionManager extends JdbcTransactionManager {

    // DataSourceではなくDb02DataSourceをDIしているのがポイント
    public Db02ransactionManager(Db01DataSource dataSource) {
        super(dataSource);
    }
}

Bean定義

作成したサブクラスたちをBean定義していきます。

@ConfigurationProperties を付加することで、 prefix で指定したプロパティが適用されるようにします。

Db01Config.java
@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);
    }
}
Db02Config.java
@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);
    }
}

リポジトリクラス

Db01SampleRepository.java
@Repository
public class Db01SampleRepository {

    private final Db01JdbcTemplate jdbcTemplate;

    // JdbcTemplateではなくDb01JdbcTemplateをDIしているのがポイント
    public Db01SampleRepository(Db01JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    ...
}
Db02SampleRepository.java
@Repository
public class Db02SampleRepository {

    private final Db02JdbcTemplate jdbcTemplate;

    // JdbcTemplateではなくDb02JdbcTemplateをDIしているのがポイント
    public Db02SampleRepository(Db02JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    ...
}

サービスクラス

Db01SampleService.java
@Service
public class Db01SampleService {

    private final Db01SampleRepository sampleRepository;

    public Db01SampleService(Db01SampleRepository sampleRepository) {
        this.sampleRepository = sampleRepository;
    }

    @Db01Transactional(readOnly = true)
    public List<Sample> findAll() {
        ...
    }
}
Db02SampleService.java
@Service
public class Db02SampleService {

    private final Db02SampleRepository sampleRepository;

    public Db02SampleService(Db02SampleRepository sampleRepository) {
        this.sampleRepository = sampleRepository;
    }

    @Db02Transactional(readOnly = true)
    public List<Sample> findAll() {
        ...
    }
}

なぜこの方法がオススメなのか

理由は2つあります。

  1. 型でDIするので間違いにくくなる
  2. トランザクションマネージャーのロガー名が各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アノテーションを作成する

Db01.java
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD})
@Qualifier
public @interface Db01 {
}
Db02.java
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PARAMETER, ElementType.FIELD})
@Qualifier
public @interface Db02 {
}

Bean定義

Beanを定義している箇所、およびそれらをDIしている箇所の両方に、作成したカスタム @Qualifier アノテーションを負荷します。

Db01Config.java
@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);
    }
}
Db02Config.java
@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);
    }
}

リポジトリクラス

Db01SampleRepository.java
@Repository
public class Db01SampleRepository {

    private final JdbcTemplate jdbcTemplate;

    public Db01SampleRepository(@Db01 JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    ...
}
Db02SampleRepository.java
@Repository
public class Db02SampleRepository {

    private final JdbcTemplate jdbcTemplate;

    public Db02SampleRepository(@Db02 JdbcTemplate jdbcTemplate) {
        this.jdbcTemplate = jdbcTemplate;
    }

    ...
}

サービスクラス

方法①と同じなので省略します。

注意点

JUnitテストクラスで @Sql を使うときに注意点があります。

詳細は👇の記事をご覧ください。

https://qiita.com/suke_masa/items/1461166badd9f94abcb2

Discussion