しおしお

IntelliJ IDEAのことなんかを書いてます

Spring Data R2DBCで複数のデータベースに接続してみる

やりたいこと

単一のWebFluxのアプリケーションで、Spring Data R2DBCを使って複数のデータベースに接続してSQLを実行したい。

環境準備

データベース

準備が簡単なので、Docker Composeを使ってデータベースを2つ起動しています。

version: '3.7'
services:
  db1:
    image: 'postgres'
    ports:
      - "15432:5432"
    environment:
      - POSTGRES_HOST_AUTH_METHOD=trust
  db2:
    image: 'postgres'
    ports:
      - "15433:5432"
    environment:
      - POSTGRES_HOST_AUTH_METHOD=trust

テーブル

アプリケーションで使用するテーブルをそれぞれのデータベースに作成してあげます。

db1

create table hoge (
    id   char(36) not null,
    name varchar(100),
    primary key (id)
);

db2

create table fuga (
    id   serial not null,
    name varchar(100),
    primary key (id)
);

アプリケーションの実装

Spring Bootバージョン

Spring Bootは現時点で最新のバージョンを使って試してみます。

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.4</version>
    <relativePath /> <!-- lookup parent from repository -->
  </parent>

データベースの接続を切り替えるための実装

Configurationの準備

db1とdb2に接続するためのConfigurationを準備します。

Configurationには@EnableR2dbcRepositoriesアノテーションを追加し、この接続設定を使うベースパッケージをbasePackagesに指定します。 また、指定したベースパッケージ配下で利用するR2dbcEntityTemplateのBean名をentityOperationsRefに指定します。 これらを設定してあげるだけで、パッケージ単位に利用するR2dbcEntityTemplateが自動的に切り替わるので、複数のデータベースへのアクセスが可能になります。

@Configuration
@EnableR2dbcRepositories(
    basePackages = ["siosio.multipler2dbc.db1"],
    entityOperationsRef = "db1EntityTemplate"
)
class Db1Config {

    @Bean
    @ConfigurationProperties(prefix = "db1")
    fun db1Properties(): R2dbcProperties = R2dbcProperties()

    @Bean
    fun db1ConnectionFactory(): ConnectionFactory = db1Properties().toConnectionFactory()

    @Bean
    fun db1EntityTemplate() = R2dbcEntityTemplate(db1ConnectionFactory())
}

@Configuration
@EnableR2dbcRepositories(
    basePackages = ["siosio.multipler2dbc.db2"],
    entityOperationsRef = "db2EntityTemplate"
)
class Db2Config {

    @Bean
    @ConfigurationProperties(prefix = "db2")
    fun db2Properties(): R2dbcProperties = R2dbcProperties()

    @Bean
    fun db2ConnectionFactory(): ConnectionFactory = db2Properties().toConnectionFactory()

    @Bean
    fun db2EntityTemplate() = R2dbcEntityTemplate(db2ConnectionFactory())
}

fun R2dbcProperties.toConnectionFactory(): ConnectionFactory {
    val connectionFactory = ConnectionFactoryOptions.parse(url)
        .mutate()
        .apply {
            username?.let { option(ConnectionFactoryOptions.USER, it) }
            password?.let { option(ConnectionFactoryOptions.PASSWORD, it) }

        }.build().let {
            ConnectionFactories.get(it)
        }
    val poolConfiguration = ConnectionPoolConfiguration
        .builder(connectionFactory)
        .maxSize(pool.maxSize)
        .initialSize(pool.initialSize)
        .build()
    return ConnectionPool(poolConfiguration)
}

接続先の設定

db1db2に対する接続先の情報をproperties(yml)に設定します。 今回は、db1db2としてConfigurationクラスにプレフィックスと指定しているので、設定値はdb1.またはdb2.から始まります。 プレフィクス以降は、R2dbcPropertiesに対して設定可能なキーを指定してあげます。

db1.url=r2dbc:postgresql://localhost:15432/postgres
db1.username=postgres

db2.url=r2dbc:postgresql://localhost:15433/postgres
db2.username=postgres

データベースに対するアクセスする側の実装

それぞれのデータベースに対してアクセスするには、Configurationで指定したパッケージ配下にデータベースに対する処理を実装するだけとなります。 今回の例では、次ののように実装するだけで自動的に接続先が切り替わります。

  • db1に対する処理は、siosio.multipler2dbc.db1パッケージ配下に実装します。
  • db2に対する処理は、siosio.multipler2dbc.db2パッケージ配下に実装します。

あとは、単一データベース接続のときと同じようにデータベースアクセスするだけですね。1

db1への処理

package siosio.multipler2dbc.db1

// importは省略

@Component
class Db1Handler(private val hogeRepository: HogeRepository) {
    suspend fun post(request: ServerRequest) : ServerResponse {
        val hoge = hogeRepository.save(Hoge(UUID.randomUUID().toString(), "hoge")).awaitSingle()
        return ServerResponse.ok().bodyValueAndAwait(mapOf("id" to hoge.id))
    }
}

@Repository
interface HogeRepository: ReactiveCrudRepository<Hoge, String>

//create table hoge (id char(36) not null, name varchar(100), primary key(id))
data class Hoge(
    @Id private val id: String,
    val name: String
) : Persistable<String> {
    override fun isNew(): Boolean = true
    override fun getId() = id
}

db2への処理

package siosio.multipler2dbc.db2

// importは省略

@Component
class Db2Handler(private val fugaRepository: FugaRepository) {
    suspend fun post(request: ServerRequest): ServerResponse {
        val fuga = fugaRepository.save(Fuga(name = "fuga")).awaitSingle()
        return ServerResponse.ok().bodyValueAndAwait(mapOf("id" to fuga.id))
    }
}

@Repository
interface FugaRepository : ReactiveCrudRepository<Fuga, Int>

//create table fuga (id serial not null, name varchar(100), primary key(id))
data class Fuga(
    @Id val id: Int? = null,
    val name: String
)

動作確認

curlでそれぞれのエンドポイントを叩くと、エラーなど起きずにデータベースの処理結果のIDが返ってくることが確認できます。 これで、複数データベースへの接続確認はOKですね。

$ curl -X POST http://localhost:8080/db1
{"id":"6c1d2c95-d36f-4b1d-acea-366f6a45e240"}

$ curl -X POST http://localhost:8080/db2
{"id":9}

ソースコード

使用したサンプルプロジェクト全体は、以下から確認できます。 github.com


  1. この実装例では、トランザクション制御は省略しています