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 につなぐアプリケーションが書ける…
しかし、残念なことに、このアプリケーションはトランザクションを貼っていない(+ コミットしていない)ので、別のインスタンスを立てると、データの状態が同期できないダメダメなアプリになっている…
また、僕たちが作りたいのは複数のテーブルまとめてコミットするかロールバックするアプリケーションなので、これでは足りない…
というわけで、トランザクションを管理できるアプリも作れたのだけど、長いので一旦ここで終わり