【Unit4 ブログリレー5日目】
こんにちは。Unit4 Eng の西川です。JavaScript が好きですが、今回は JavaScript ではなく、最近やった Spring Boot + Flyway を利用しているシステムから別の DB への接続を増やした時の話をしたいと思います。
動作環境
今回のプロダクトで利用している Spring Boot と Flyway のバージョンは以下のとおりです。 DB フレームワークは MyBatis、テストは JUnit を利用しています。
Java 17
Kotlin 1.7.20
Spring Boot 2.7.4
Flyway 7.11.3
PostgreSQL 42.4.1
背景
そもそもどうして複数の DB に接続する必要が出たかというお話なのですが、Unit4 ではかなり多くのサービスを扱っているため、どうしても保守が間に合わないサービスが出てきてしまいます。そこで、とあるレガシーなサービスを比較的新しいサービスに持ってくる際に、先に API や画面を持ってきて、最後に DB を持ってこようとしたわけです。
API や画面を持ってきている間は、旧サービスの方の DB を見に行く必要があります。だから、複数の DB に接続する必要があったんですね。
なお、移行元のサービスには Flyway が入っておらず、移行先のサービスにのみ入っている状態です。
Spring Boot で複数の DB に接続する
とりあえず、Spring Boot で複数の DB に接続する方法を調べました。
概要
Spring Boot(+ MyBatis)で必要な Bean は以下のとおりです。
- DataSource
- TransactionManager
- SqlSessionFactory
(MyBatis 用)
Spring Boot は Auto-Configuration*1 で、application.yaml
(orapplication.properties
) に記載されている情報を元に、自動で Bean を作成してくれます。
以下のように application.yaml
に spring.datasource
として DB の接続情報を記載すると、その情報を元に上記の Bean を作ってくれる感じです。
spring: datasource: driver-class-name: org.postgresql.Driver url: ${POSTGRES_URL} username: ${POSTGRES_USER} password: ${POSTGRES_PASSWORD}
上述した Auto-Configuration で作成してくれる各種 Bean はひとつずつであるため、複数利用したい場合には手動で設定する必要があります。今回は 2 台の DB に接続したいので、2 台分の DB 接続情報を設定する形になります。*2
具体的に言うと、以下のステップを踏む必要があります。順番に見ていきます。
application.yaml
の記載- Configuration ファイルの作成
application.yaml
の記載
最初に application.yaml
の spring.datasource
に、 2 台分の DB 接続情報を記載します。
spring: datasource: datasourceA: driver-class-name: org.postgresql.Driver url: ${A_POSTGRES_URL} username: ${A_POSTGRES_USER} password: ${A_POSTGRES_PASSWORD} datasourceB: driver-class-name: org.postgresql.Driver url: ${B_POSTGRES_URL} username: ${B_POSTGRES_USER} password: ${B_POSTGRES_PASSWORD}
Configuration ファイルの作成
次に、Bean の定義を記載する Configuration ファイルを作成します。今回はそれぞれ以下の場所に置きました。
└── adapter └── gateway └─ db ├── datasourceA │ ├─ datasourceAConfiguration.kt │ └─ datasourceAMapper.kt └── datasourceB ├─ datasourceBConfiguration.kt └─ datasourceBMapper.kt
Configuration ファイルの中身は以下のようになります。
@Configuration @MapperScan(sqlSessionFactoryRef = "datasourceASqlSessionFactory") class DatasourceADbConfiguration { @Bean @ConfigurationProperties(prefix = "spring.datasource.datasourceA") fun datasourceAProperties(): DataSourceProperties { return DataSourceProperties() } @Bean(name = ["datasourceA"]) fun datasourceA( @Qualifier("datasourceAProperties") properties: DataSourceProperties ): DataSource { return properties.initializeDataSourceBuilder().build() } @Bean(name = ["datasourceATxManager"]) fun datasourceATxManager(datasourceADataSource: DataSource): PlatformTransactionManager { return DataSourceTransactionManager(datasourceADataSource) } @Bean(name = ["datasourceASqlSessionFactory"]) @Throws(Exception::class) fun sqlSessionFactory(@Qualifier("datasourceA") datasourceADataSource: DataSource?): SqlSessionFactory? { val sqlSessionFactory = SqlSessionFactoryBean() sqlSessionFactory.setDataSource(datasourceADataSource) return sqlSessionFactory.getObject() } }
@MapperScan
や @Bean(name = ["datasourceASqlSessionFactory"])
は MyBatis 用の設定で、それ以外が Spring Boot の設定になります。
上述の application.yaml
の設定値を @ConfigurationProperties(prefix = "spring.datasource.datasourceA")
で読み取った DataSourceProperties
から、 DataSource
の Bean を作成し、作成された DataSource
を元に TransactionManager
の Bean を作成しているのがわかるかと思います。
これでそれぞれの DB に接続していい感じに動いてくれるようになりました。
Flyway を複数の DB に適用する
さて、これで問題なし……と思いきや、移行元のサービスでは Flyway を入れておらず、移行先のサービスでは入れているため、そのまま起動しようとするとエラーになってしまいます。
Caused by: org.flywaydb.core.api.FlywayException: Found non-empty schema(s) "public" but no schema history table. Use baseline() or set baselineOnMigrate to true to initialize the schema history table.
Flyway は起動時に flyway_schema_history
というテーブルから現在のマイグレーションの状態を確認して、同じ変更を適用しないようにしてくれるのですが、DB に既にテーブルがあるのに flyway_schema_history
テーブルがない場合はエラーになってしまうんですね。
これを回避するためには flyway_schema_history
を手動で作成するか、Flyway の baseline
コマンドでベースライン(Flyway の適用が開始された状態)を設定するか、baselineOnMigrate
でベースラインを設定するかのどれかをする必要があります。今回は一番最後の、baselineOnMigrate
でベースラインを設定する方式にしました。
baselineOnMigrate を適用する
baselineOnMigrate
はだいたいの場合は以下のように設定すれば、先述した Spring Boot の Auto-Configuration で Flyway
の Bean を作って適用してくれます。
設定値は以下をご参照ください。
- Baseline On Migrate - Flyway - Product Documentation
- Baseline Version - Flyway - Product Documentation
spring: flyway: enabled: true clean-on-validation-error: false baseline-on-migrate: true baseline-version: 1
Configuration ファイルの作成
しかし、このやり方だと複数の DB がある時に適用できません。Flyway
の Bean も、DataSource
と同じように手動で作成する必要があります。今回は Configuration ファイルを以下の場所に置きました。
src/main ├── kotlin/com/m3/service │ ├── adapter │ │ └── gateway │ │ └─ db │ │ ├── datasourceA │ │ └── datasourceB │ └── config │ └── FlywayConfiguration.kt │ └── resource/db/migration ├── datasourceA │ └── V1__init_datasourceA.sql └── datasourceB └── V1__init_datasrouceB.sql
Configuration ファイルの中身は以下のようになります。
@Configuration class FlywayConfiguration { // すでに存在する DB に適用するため、baselineOnMigrate を true にする // init ファイルを適用したくないので、baselineVersion を 1 にする @Bean(name = ["datasourceAFluentConfiguration"]) fun datasourceADbFluentConfiguration(): FluentConfiguration { return FluentConfiguration().baselineOnMigrate(true).baselineVersion("1") } @Bean(name = ["datasourceBFluentConfiguration"]) fun datasourceBFluentConfiguration(): FluentConfiguration { return FluentConfiguration() } @Bean(name = ["datasourceAFlyway"], initMethod = "migrate") fun createDatasourceAFlyway( @Qualifier("datasourceAFluentConfiguration") fluentConfiguration: FluentConfiguration, @Qualifier("datasourceA") dataSource: DataSource? ): Flyway { return Flyway( fluentConfiguration.locations("db/migration/datasourceA").dataSource(dataSource) ) } @Bean(name = ["datasourceBFlyway"], initMethod = "migrate") fun createDatasourceBFlyway( @Qualifier("datasourceBFluentConfiguration") fluentConfiguration: FluentConfiguration, @Qualifier("datasourceB") dataSource: DataSource? ): Flyway { return Flyway( fluentConfiguration.locations("db/migration/datasourceB").dataSource(dataSource) ) } }
FluentConfiguration
は、Flyway
の Bean で利用する設定を入れておく Bean です。本当は @ConfigurationProperties
を利用して、上記の application.yaml
で設定した内容を読み取らせるのですが、なぜかうまくいかなかったので、直指定しています。datasourceB
の方は既存 DB で最初から Flyway が入っていたので、特に設定を追加していません。datasourceA
の方は接続を追加する DB で、Flyway 自体を追加する必要があったため、init ファイルのバージョン(1
)を指定することで、初期テーブルの作成はされているという状態で Flyway を開始するようにしました。
その下の createDatasourceAFlyway
メソッドで、実際の Flyway
の Bean を作成しています。引数で指定する FluentConfiguration
は上記で作成した Bean を*3、DataSource
は、Spring Boot を複数 DB に接続するための Configuration に記載した Bean を指定します。それぞれにつけた名前を使って @Qualifier
で呼び出します。
これで起動した時に、datasourceA は createDatasourceAFlyway
で作成された Bean を、datasourceB は createDatasourceBFlyway
で作成された Bean を使って Flyway を起動してくれるわけです。
テスト用の Configuration ファイルを作成
これで全ての工程が完了かと思えば、テストがいい感じに動きません。テストの時は init から migrate して欲しいのですが、そこの指定をしないといけません。そこで、テストの Mapper を置くディレクトリに、テスト用の Configuration ファイルを置きました。
src ├── main │ ├── kotlin/com/m3/service │ │ ├── adapter │ │ │ └── gateway │ │ │ └─ db │ │ │ ├── datasourceA │ │ │ └── datasourceB │ │ └── config │ │ └── FlywayConfiguration.kt │ │ │ └── resource/db/migration │ ├── datasourceA │ └── datasourceB │ └── test └── kotlin/com/m3/service └── adapter └── gateway └─ db ├── datasourceA │ └── DatasourceAConfigurationTest.kt └── datasourceB
中身は以下のようになります。
@Bean(name = ["datasourceAFlyway"], initMethod = "migrate") fun createDatasourceAFlyway(@Qualifier("datasourceA") dataSource: DataSource?): Flyway { return Flyway( FluentConfiguration().locations("db/migration/datasourceA").dataSource(dataSource) ) }
FluentConfiguration
を設定せず、初期状態で migrate するようにした、というだけですね。これでテストでは init から、通常起動では init は実行されずに Flyway 管理されるようになりました!
これで DB をいじった履歴が残って、誰がやらかしたかわかりますね!(怖い!)
終わりに
いかがでしたか?
今回のようにレガシーなシステムに追加で Flyway を入れるというのはままあることだと思いますが、別々で利用するというのはなかなかないのかもなと思いました。誰かの助けになれば幸いです。
We're hiring!
エムスリーでは一緒に働くエンジニアを募集しています。Spring Boot 以外にもさまざまなフレームワーク/言語のサービスを取り扱っているので、興味がある方は以下からお問い合わせください!
*1:https://docs.spring.io/spring-boot/docs/2.0.x/reference/html/using-boot-auto-configuration.html
*2:手動で Bean が定義されていない場合のみ、Auto-Configuration で Bean が作成されるので、2 台以上の場合は全て手動で記載する必要があります
*3:datasourceB の方は追加で設定をしていないので、FluentConfiguration を明示的に指定する必要はないのですが、わかりやすくするために設定しています