Spring Boot: 複数DB利用時に初期化が期待通りに行われない問題と解決法
概要
複数DB利用時、初期化スクリプト(schema.sql
やdata.sql
)が適切に実行されないという問題が発生。
具体的には、@Primary
指定したDataSource
では実行されるが、それ以外では実行されずテーブル作成などの初期化処理が行われないというもの。
調査の結果、仕組み上このような動作になるのは仕方がないことがわかった(仕様なのかはどうかは不明)。
この問題が発生する理由と解決法をまとめる。
検証環境
- Spring Boot 2.1.6.RELEASE
- Spring Data JPA
- H2
解決法
まず先に解決法を整理。
ざっくり結論を書くと「DataSource
ごとにDataSourceInitializer
をBean定義する」ことで解決する。
今回は "user" と "book" という2つのDB(どちらもH2)に接続するという前提にする。
application.yaml
上のDataSource
に関する設定は以下のようにする。
※ initialization-mode: never
を忘れずにつけること。 *1
app: datasource: userdb: url: jdbc:h2:mem:userdb driver-class-name: org.h2.Driver username: sa password: password schema: classpath:schema-userdb.sql data: classpath:data-userdb.sql initialization-mode: never bookdb: url: jdbc:h2:mem:bookdb driver-class-name: org.h2.Driver username: sa password: password schema: classpath:schema-bookdb.sql data: classpath:data-bookdb.sql initialization-mode: never
まず "user" DB側のConfiguration。こちらをPrimaryにする。
今回重要なのはDataSourceInitializer
のBean定義の箇所。
@Configuration @EnableJpaRepositories( basePackages = "demo.multidb.repository.userdb", entityManagerFactoryRef = "userEntityManagerFactory", transactionManagerRef = "userTransactionManager" ) public class UserDbConfig { @Primary @Bean(name = "userDataSourceProperties") @ConfigurationProperties(prefix = "app.datasource.userdb") public DataSourceProperties dataSourceProperties() { return new DataSourceProperties(); } @Primary @Bean(name = "userDataSource") public DataSource dataSource( @Qualifier("userDataSourceProperties") DataSourceProperties dataSourceProperties) { return dataSourceProperties.initializeDataSourceBuilder().build(); } @Primary @Bean(name = "userEntityManagerFactory") public LocalContainerEntityManagerFactoryBean entityManagerFactory( EntityManagerFactoryBuilder builder, @Qualifier("userDataSource") DataSource dataSource) { return builder .dataSource(dataSource) .packages("demo.multidb.entity.userdb") .persistenceUnit("userPU") .build(); } @Primary @Bean(name = "userTransactionManager") public PlatformTransactionManager transactionManager( @Qualifier("userEntityManagerFactory") EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); } @Primary @Bean(name = "userDataSourceInitializer") public DataSourceInitializer dataSourceInitializer( @Qualifier("userDataSourceProperties") DataSourceProperties dataSourceProperties, @Qualifier("userDataSource") DataSource dataSource) { ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); ResourceLoader resourceLoader = new DefaultResourceLoader(); Stream.concat(dataSourceProperties.getSchema().stream(), dataSourceProperties.getData().stream()) .map(resourceLoader::getResource) .forEach(populator::addScript); DataSourceInitializer dataSourceInitializer = new DataSourceInitializer(); dataSourceInitializer.setDataSource(dataSource); dataSourceInitializer.setDatabasePopulator(populator); return dataSourceInitializer; } }
次に "book" DB側のConfiguration。
@Configuration @EnableJpaRepositories( basePackages = "demo.multidb.repository.bookdb", entityManagerFactoryRef = "bookEntityManagerFactory", transactionManagerRef = "bookTransactionManager" ) public class BookDbConfig { @Bean(name = "bookDataSourceProperties") @ConfigurationProperties(prefix = "app.datasource.bookdb") public DataSourceProperties dataSourceProperties() { return new DataSourceProperties(); } @Bean(name = "bookDataSource") public DataSource dataSource( @Qualifier("bookDataSourceProperties") DataSourceProperties dataSourceProperties) { return dataSourceProperties.initializeDataSourceBuilder().build(); } @Bean(name = "bookEntityManagerFactory") public LocalContainerEntityManagerFactoryBean entityManagerFactory( EntityManagerFactoryBuilder builder, @Qualifier("bookDataSource") DataSource dataSource) { return builder .dataSource(dataSource) .packages("demo.multidb.entity.bookdb") .persistenceUnit("bookPU") .build(); } @Bean(name = "bookTransactionManager") public PlatformTransactionManager transactionManager( @Qualifier("bookEntityManagerFactory") EntityManagerFactory entityManagerFactory) { return new JpaTransactionManager(entityManagerFactory); } @Bean(name = "bookDataSourceInitializer") public DataSourceInitializer dataSourceInitializer( @Qualifier("bookDataSourceProperties") DataSourceProperties dataSourceProperties, @Qualifier("bookDataSource") DataSource dataSource) { ResourceDatabasePopulator populator = new ResourceDatabasePopulator(); ResourceLoader resourceLoader = new DefaultResourceLoader(); Stream.concat(dataSourceProperties.getSchema().stream(), dataSourceProperties.getData().stream()) .map(resourceLoader::getResource) .forEach(populator::addScript); DataSourceInitializer dataSourceInitializer = new DataSourceInitializer(); dataSourceInitializer.setDataSource(dataSource); dataSourceInitializer.setDatabasePopulator(populator); return dataSourceInitializer; } }
あとは以下のファイルを用意すれば完成。
src/main/resources/schema-userdb.sql
src/main/resources/data-userdb.sql
src/main/resources/schema-bookdb.sql
src/main/resources/data-bookdb.sql
原因詳細
ざっと原因を書くと「DataSourceInitializerInvoker
がsingletonで、DataSourceInitializerInvoker#afterPropertiesSet
が1度しか呼ばれないため」となる。
初期化スクリプトが実行されるまでの処理を追っていくと以下のようになる。
DataSourceInitializationConfiguration
がDataSourceInitializerInvoker
とDataSourceInitializationConfiguration.Registrar
をImport。DataSourceInitializationConfiguration.Registrar#registerBeanDefinitions
が呼ばれ、DataSourceInitializerPostProcessor
がBean定義される。DataSourceInitializerPostProcessor#postProcessAfterInitialization
が呼ばれ、DataSourceInitializerInvoker
のインスタンスが生成される。DataSourceInitializerInvoker#afterPropertiesSet
が呼ばれる。DataSourceInitializer#createSchema
が呼ばれる。- 初期化スクリプト実行。
手順3のDataSourceInitializerPostProcessor#postProcessAfterInitialization
呼び出しの箇所はDataSourceごとに実行される。
しかし、DataSourceInitializerInvoker
がsingletonであるため、DataSourceInitializerInvoker#afterPropertiesSet
が最初の1回しか呼ばれず、後続処理も1回しか実行されない結果となる。
参考資料
Spring Bootにおける複数DB利用は公式ドキュメントに一応記載がある。が、ちょっと記載不足を感じる。
複数DB利用の記事はたくさんあるが、この記事が一番簡潔でわかりやすいかもしれない。
Using multiple datasources with Spring Boot and Spring Data 💈 ⇄🌱 ⇄ 💈
この問題に関するStack Overflowでのスレッド。回答者は "expected behavior" と書いているが本当にそうなのか?期待動作の定義は特にされておらず、たまたまそうなっただけにも感じる。
java - Spring Boot 2 Multiple Datasources initialize schema - Stack Overflow
*1:"initialization-mode: never"を忘れると、Primary側のDataSourceはデフォルトの初期化処理が動いてしまい、今回自前で用意したDataSourceInitializerの処理と重複してしまう。