Springの小話 - DataSourceのConfigを理解しよう
Springといっても今回はSpring BootのDataSourceのConfig(構成)の小話です。
DataSourceの構成はAutoConfigurationがspring.datasource.*の設定で色々と自動でやってくれて便利ですが、その一方でデバッグ時にこれどこで設定されてるのだっけ?と悩んだりすることはありませんか。私はなんど理解しても綺麗に忘れます。そこで今回は備忘録を兼ねAutoConfigurationを使わず素の状態でDataSourceを構成する方法を説明したいと思います。素の構成を理解することでAutoConfigurationの裏で行われていることが分かってくるかと思います。
DataSourceの構成は「データアクセス :: Spring Boot - リファレンスドキュメント」で説明されていますが、細かい内部動作までは説明されていないため、今回はこの内容を補足する形で説明しています。
パターン1:基本となる一番シンプルな構成
#まずは設定されている内容を単にDataSourceにバインドするだけの一番シンプルな構成例を見ていきましょう。この構成は次のようになります。
この記事はSpring Boot 3.2.6で動作を確認しています。またspring-boot-starter-data-jpaの推移的依存によりH2 DatabaseとHikariCPがclasspath上にあることを前提に説明を行います。
app:
datasource:
jdbc-url: jdbc:h2:mem:mydb
driver-class-name: org.h2.Driver
username: sa
password: pass
maximum-pool-size: 30
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties
class DataSourceConfig {
@Bean
@ConfigurationProperties("app.datasource") // (1)
DataSource dataSource() {
return DataSourceBuilder.create().build(); // (2)
}
}
この構成によりDataSourceインスタンスがBeanとして登録される流れは次のようになります。
- (2)でSpringで標準サポートされるDataSource実装がclasspath上にあるかの確認が行われ、ある場合はその実装クラスのインスタンスが
DataSourceBuilderのbuild()メソッドで生成されます。 DataSourceBuilderのbuild()ではclasspathの状態に応じたDataSourceインスタンスが生成されるため、DataSource実装を明示する必要はありません。- ただし(2)で行われるのは単なるDataSourceのインスタンス生成のみでドライバークラス名や接続URLなどDB接続に必要なプロパティの設定は行われていません。
- このためDataSourceインスタンスに接続に必要なプロパティを設定する必要がありますが、これをやっているのが(1)になります。
- (1)の
@ConfigurationPropertiesがあることで、app.datasource以下の設定がdatasource()メソッドから返されたDataSourceインスタンスにバインドされます。 - インスタンスへのバインドは
@ConfigurationPropertiesの機能で行われるため、この作法に従いapp.datasourceのキー名は生成されるDataSourceインスタンスのプロパティ名に合わせる必要があります。
@ConfigurationPropertiesはメソッドではなくバインドするクラスにつけ@EnableConfigurationPropertiesにそのクラスを指定して有効化する次の使い方が一般的です。
@EnableConfigurationProperties(SomeProperties.class)
class DataSourceConfig {
@ConfigurationProperties(prefix = "app.datasource")
class SomeProperties {
...
これに対して今回のDataSourceの構成例は@ConfigurationPropertiesがメソッドについています。指定の意味は上述のとおりメソッドから返されたインスタンスに指定された設定をバインドするとなりますが、これを有効化するには実行時のコンテキストに@EnableConfigurationPropertiesが指定されている必要があります。
@ConfigurationPropertiesのバインド処理はConfigurationPropertiesBindingPostProcessorにより行われますが、このPostProcessorは@EnableConfigurationPropertiesがコンテキス含まれていることにより登録されます。したがって@EnableConfigurationPropertiesで指定するクラスがない場合は、今回の構成例のようにクラスは指定せず@EnableConfigurationProperties単独で指定する必要があります。ちなみに私はこの設定がわからず3時間くらいSpring Bootのコードと格闘しました...
パターン2:DataSource実装を指定したシンプルな構成
#上述のパターン1は利用するDataSource実装が自動で決定されますが、classpath上に複数のDataSource実装がある場合、利用する実装を自分で明示したい場合があります。このような場合は次のようにして生成するDataSourceを指定することもできます。
@Bean
@ConfigurationProperties("app.datasource")
public DataSource dataSource() {
DataSourceBuilder.create()
.type(HikariDataSource.class) // (1)
.build();
}
※設定はパターン1と同じ
利用するDataSourceを明示する場合は(1)のtype()メソッドでDataSource実装を指定します。
パターン3:DataSourcePropertiesによる統一的なプロパティ設定
#これまで見てきた2つのパターンはいずれもDataSource実装が持つプロパティをそのまま設定ファイルに指定する必要がありました。
例えば、HikariCPの接続URLプロパティがjdbcUrl(jdbc-url)なのに対して、Oracle UCPはurlとなります。また、接続ドライバークラス名はHikariCPがdriverClassName(driver-clss-name)なのに対して、Oracle UCPはconnectionFactoryClassName(connection-factory-class-name)となります。
いずれも意味的に同じものですが、実装ごとにそのプロパティ名の確認が必要となるのはもちろんのこと、設定の柔軟性も損なわれます。
この面倒くささを低減させてくれる機能としてSpring BootではDataSourcePropertiesが提供されています。DataSourcePropertiesは接続URL、ドライバークラス名、接続ユーザ、接続パスワードの4つのプロパティをDataSourceの実装に依らず、統一して扱えるようにしてくれます。この機能を利用した例が次になります。
app:
datasource:
url: jdbc:h2:mem:mydb # jdbc-urlではなくurl
driver-class-name: org.h2.Driver
username: sa
password: pass
maximum-pool-size: 30
@Bean
@ConfigurationProperties("app.datasource") // (1)
DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
@Bean
DataSource dataSource(DataSourceProperties properties) { // (2)
return properties.initializeDataSourceBuilder() // (3)
.type(HikariDataSource.class) // (4)
.build(); // (5)
}
この構成によりDataSourceインスタンスがBeanとして登録される流れは次のようになります。
- (1)により
app.datasource以下の設定がDataSourcePropertiesインスタンスにバインドされる。 - (1)のインスタンスが(2)の引数として渡される。
- (3)の
initializeDataSourceBuilder()でDataSourcePropertiesにバインドされた設定が引き継がれたDataSourceBuilderが生成される。 - (4)で生成するDataSource実装を指定。
- (5)の
build()メソッドで(4)で指定されたDataSourceインスタンスが生成されるとともに、urlはjdbc-urlといったようにDataSourceに応じたプロパティ名の解決が行われ、プロパティに値が設定されたDataSourceインスタンスが生成される。
このようにDataSource実装とDataSourcePropertiesのプロパティ名にギャップがある場合、DataSourceBuilderによりプロパティのマッピングが行われるため、DataSource実装に依らない統一的なプロパティ設定が可能となります。
パターン4:DataSourcePropertiesを使った固有プロパティの設定
#パターン3ではシレっとmaximum-pool-sizeがどうなるかを説明しませんでしたがこの設定はどうなるのでしょう?その答えは「設定されません」となります。
DataSourcePropertiesにバインドされる設定はDataSourcePropertiesがサポートするurl, driver-class-name, name, passwordの4つのみです。@ConfigurationProperties("app.datasource")の指定によりapp.datasource以下の5つの設定のバインドがDataSourcePropertiesに対して試みられますが、maximum-pool-sizeを受け取るプロパティはないため無視され、結果としてDataSourceBuilderには渡りません。
このためDataSourcePropertiesにないDataSource実装に固有な設定を行う場合は、DataSourceBuilderではなく、固有設定を別のネームスペースで定義し、その設定を@ConfigurationPropertiesでDataSourceインスタンスにバインドするようにします。この構成の例が次になります。
app:
datasource:
url: jdbc:h2:mem:mydb
...(パターン3と同じ)
configuration: # 固有設定としてネームスペースを追加する
maximum-pool-size: 30
@Bean
@ConfigurationProperties("app.datasource") // (1)
DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("app.datasource.configuration") // (3)
DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build(); // (2)
}
(1)から(2)までは上述のパターン4と全く同じでDataSourcePropertiesがサポートする4つのプロパティが設定されたDataSourceインスタンスが返されます。
違いは(3)になります。
(3)によりdatasource()メソッドから返されたインスタンスに対してapp.datasource.configurationの設定がバインドされ、結果HikariDataSourceインスタンスのmaximumPoolSizeプロパティにapp.datasource.configuration.maximum-pool-sizeの30が設定されます。
このようにDataSourcePropertiesにないDataSource実装固有な設定を行う場合は、ネームスペースを別に定義し、インスタンス生成後に@ConfigurationPropertiesでバインドするようにします。
パターン5:DataSourcePropertiesによるプロパティの自動設定
#ここまでの例ではDB接続に必要な設定をすべて明示的に設定していましたが、classpathの内容をもとに自動で設定してもらうこともできます。利用するDBがH2のような組み込みDBであれば、次にように共通プロパティの設定をすべて不要とすることも可能です。
app:
datasource:
configuration:
maximum-pool-size: 30
※JavaConfigの実装はパターン4と同じものを再掲
@Bean
@ConfigurationProperties("app.datasource")
DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("app.datasource.configuration")
DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder() // (1)
.type(HikariDataSource.class)
.build(); // (2)
}
これまでは共通となる4つのプロパティがすべて設定されている例でしたが、今回の例はDataSourcePropertiesにプロパティは設定されていません。このような値が設定されていないプロパティに対してはinitializeDataSourceBuilder()メソッドで設定値が補完されます。この補完される設定値は次のようになります。
driverClassNameプロパティurlプロパティが設定されている場合はそのURLもとに対応するドライバークラスを補完。これはjdbc:h2:mem:mydbのようにの接続URLのjdbc:の次がdatabase-typeになっていることをもとにしています。なお、Spring Bootで自動設定がサポートされるDBはDatabaseDriverに記載のとおりとなります。urlプロパティが設定されていない場合は、classpath上に組み込みDBクラスがあるかを確認し、あればそのドライバークラスをdriverClassNameとして補完。なお、自動設定がサポートされる組み込みDBはEmbeddedDatabaseConnectionに記載のとおりとなります。- 上記以外はエラー
urlプロパティ- classpath上に組み込みDBクラスがあるかを確認し、あればその組み込みDBに対するデフォルトの接続URLを
urlプロパティとして補完(H2でればjdbc:h2:mem:%s;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSEとなり%sにはuuidが設定される) - 上記以外はエラー
- classpath上に組み込みDBクラスがあるかを確認し、あればその組み込みDBに対するデフォルトの接続URLを
usernameプロパティ- classpath上に組み込みDBクラスがあるかを確認し、あればその組み込みDBに接続するためのデフォルトのユーザ名を補完(H2であれは
sa) - 上記以外はエラー
- classpath上に組み込みDBクラスがあるかを確認し、あればその組み込みDBに接続するためのデフォルトのユーザ名を補完(H2であれは
passwordプロパティusernameと同じ。(H2であれは空文字列)
この自動設定はDataSourcePropertiesの機能となるため、AutoConfigurationは必要となりません。
パターン6:複数のDataSource設定
#ここまでの設定が理解できればこれまで難解に見えていた複数DataSourceの設定も分かってくるようになります。ということで最後に複数DataSourceの設定例を見て終わりにしたいと思います。
app:
datasource:
first:
url: "jdbc:mysql://localhost/first"
username: "dbuser"
password: "dbpass"
configuration:
maximum-pool-size: 30
second:
url: "jdbc:mysql://localhost/second"
username: "dbuser"
password: "dbpass"
max-total: 30
// 1つ目の接続構成
@Bean
@Primary
@ConfigurationProperties("app.datasource.first")
public DataSourceProperties firstDataSourceProperties() { // (1)
return new DataSourceProperties();
}
@Bean
@Primary
@ConfigurationProperties("app.datasource.first.configuration")
public HikariDataSource firstDataSource(
DataSourceProperties firstDataSourceProperties) { // (2)
return firstDataSourceProperties
.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
// 2つ目の接続構成
@Bean
@ConfigurationProperties("app.datasource.second")
public DataSourceProperties secondDataSourceProperties() { // (3)
return new DataSourceProperties();
}
@Bean
@ConfigurationProperties("app.datasource.second.configuration")
public BasicDataSource secondDataSource(
@Qualifier("secondDataSourceProperties") DataSourceProperties secondDataSourceProperties) { // (4)
return secondDataSourceProperties
.initializeDataSourceBuilder()
.type(BasicDataSource.class)
.build();
}
2つのDataSourceインスタンスがBeanとして登録されるまでの流れは次のようになります。
- (1)で
app.datasource.first以下の1つ目の接続情報をDataSourcePropertiesにバインド。 - (2)で接続情報がバインド済みの(1)の
DataSourcePropertiesをもとにDataSourceインスタンスを生成し、その後に@ConfigurationPropertiesで固有プロパティをバインド - (3)で
app.datasource.second以下の2つ目の接続情報をDataSourcePropertiesにバインド - (4)で(2)と同様に
DataSourcePropertiesからDataSouceインスタンスを生成し、そのあとに固有プロパティをバインド - この構成では
DataSourcePropertiesのインスタンスが2つ登場するため、どっちのBeanをInjectするかの指定が必要となる。(2)は@Qualifierがないため、@Primaryが指定されている(1)がInjectされる。(4)は2つ目の接続情報の(3)をInjectしてもらうため@Qualifier("secondDataSourceProperties")をつけている
ここまでの理解をもとにDataSourceAutoConfigurationの実装やspring.datasource.*の設定を改めてみてみるとこれまでとは違った見方ができるようになっているのではないでしょうか?このようなことを期待して今回の記事は終わりにしたいと思います。
