5日間休みなので、おさらいも兼ねて、久々に Spring をさわることにしている。なお、 Kotlin にしているのもおさらいを兼ねている。また Kotlin coroutine を使っていないのは r2dbc-postgres と組み合わせたときに、 ByteBuf
の readableBytes
がレスポンス読み出しの直前に 0
になってしまう不具合(?)に遭遇したので、 coroutine を使っていない。 coroutine を使わなければ今のところ、問題なさそう。
データベースの設定
データベースの設定は AbstractR2dbcConfiguration
クラスを継承したクラスを通じておこなう
@Configuration @EnableR2dbcRepositories class DatabaseConfiguration: AbstractR2dbcConfiguration() { @Bean override fun connectionFactory(): ConnectionFactory = ConnectionFactoryOptions.builder() .option(ConnectionFactoryOptions.DRIVER, "postgresql") .option(ConnectionFactoryOptions.HOST, "localhost") .option(ConnectionFactoryOptions.PORT, 5432) .option(ConnectionFactoryOptions.USER, "postgres-user") .option(ConnectionFactoryOptions.PASSWORD, "postgres-pass") .option(ConnectionFactoryOptions.DATABASE, "postgres") .build() .let { ConnectionFactories.get(it) } override fun getDialect(connectionFactory: ConnectionFactory): Dialect = PostgresDialect() }
- レポジトリーを有効にしたいので、
EnableR2dbcRepositories
アノテーションを付与している - データベースへの接続情報を
ConnectionFactoryOptions
を使って設定する。このあたりは r2dbc-postgres などの github の README が詳しい
レポジトリーインターフェース
レポジトリーインターフェースは 普通の jdbc の場合とだいたい同じ
import org.springframework.data.annotation.Id import org.springframework.data.r2dbc.function.DatabaseClient import org.springframework.data.relational.core.mapping.Column import org.springframework.data.relational.core.mapping.Table @Table("users") data class UserDb( @Id @Column("id") var id: Long?, @Column("name") var name: String?, @Column("created") var created: Instant? ) @Repository interface UserRepository : ReactiveCrudRepository<UserDb, Long>
- テーブルを表すクラスに
@Table
アノテーションを付与する - プライマリーキーに対応するフィールドに
@Id
アノテーションを付与する - 各カラムを表すフィールドには
@Column
をつける - ちなみに、これらのアノテーションは JPA のものではなくて、 spring data が独自に定義しているものなので、そこだけは注意
コントローラー
簡単なアプリケーションなので、サービスクラスは省略。コントローラーで頑張る。やった reactor だ、 coroutine 使えないから、型合わせが難しいぞ
@RestController @RequestMapping("users") class UserController(private val repository: UserRepository) { companion object { val logger: Logger = LoggerFactory.getLogger(UserController::class.java) } private val atomicLong: AtomicLong = AtomicLong() @PostMapping fun createUser(@RequestBody createUser: CreateUser): Mono<AppResponse> = Mono.just(createUser) .filter { it.name != null } .switchIfEmpty { Mono.error(AppException("invalid")) } .map { it.toDb(atomicLong.incrementAndGet()) } .flatMap { repository.save(it) } .doOnSuccess { logger.info("create user: {}", it) } .doOnError { logger.info("failed to create user: {}, error: {}", createUser, it.message, it) } .thenReturn(AppResponse(true, "success")) } data class AppResponse(val success: Boolean, val message: String) data class CreateUser(var name: String?) { fun toDb(id: Long): UserDb = UserDb(id, name ?: "", Instant.now()) }
- coroutine が使えないと、前の結果を使うのが面倒になる(ここでは面倒にならないように簡単に処理している)
こんな感じで、 r2dbc で RDB につなぐアプリケーションが書ける…
しかし、残念なことに、このアプリケーションはトランザクションを貼っていない(+ コミットしていない)ので、別のインスタンスを立てると、データの状態が同期できないダメダメなアプリになっている…
また、僕たちが作りたいのは複数のテーブルまとめてコミットするかロールバックするアプリケーションなので、これでは足りない…
というわけで、トランザクションを管理できるアプリも作れたのだけど、長いので一旦ここで終わり