Slack の Bolt フレームワークが Java でもリリースされたということなので、早速試してみた
続きを読むBolt は最先端の Slack アプリをつくるためのフレームワーク。これまでの JavaScript に加えて、今日 Java バージョンもリリースしました! ☕ 早速試してみてください! https://t.co/Jus3XOSj9c
— Slack Platform (@SlackAPI) 2020年3月20日
Slack の Bolt フレームワークが Java でもリリースされたということなので、早速試してみた
続きを読むBolt は最先端の Slack アプリをつくるためのフレームワーク。これまでの JavaScript に加えて、今日 Java バージョンもリリースしました! ☕ 早速試してみてください! https://t.co/Jus3XOSj9c
— Slack Platform (@SlackAPI) 2020年3月20日
わからなかったので、実験した。コードを読んだわけではない。
続きを読む
前回 ReactiveCrudRepository
を使ったアプリケーションの話をしたが、残念なことに今の所トランザクションを指定できないので、アプリケーションを終了したら、データはなくなるし、複数のテーブルへの操作をアトミックな操作として指定できないことをメモしておいた。
では、トランザクションをどうおこなうかだが、 TransactionalDatabaseClient
を使うようである。
Configuration
TransactionalDatabaseClient
を作るには ConnectionFactory
のインスタンスを TransactionalDatabaseClient#create
にわたすだけでよい。そうして作ったインスタンスを ビーン登録する
@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) } @Bean fun transactionalDatabaseClient(connectionFactory: ConnectionFactory): TransactionalDatabaseClient = TransactionalDatabaseClient.create(connectionFactory) }
作った TransactionalDatabaseClient
は @Repository
相当のクラスで使うのが良いと思われる。
@Repository class UserTransactionalRepository(private val dbClient: TransactionalDatabaseClient) { fun saveUser(user: User, password: UserPassword): Mono<Unit> = dbClient.inTransaction { db -> db.execute() //language=sql .sql("insert into users(id, name, created) values($1, $2, $3)") .bind("$1", user.id) .bind("$1", user.name) .bind("$1", user.created) .fetch() .rowsUpdated() .thenReturn(Unit) .flatMap { db.execute() //language=sql .sql("insert into user_password(id, password_hash, created) values($1, $2, $3, $4)") .bind("$1", user.id) .bind("$2", password.hash) .bind("$3", user.created) .fetch() .rowsUpdated() } // flatMap .thenReturn(Unit) } // inTransaction .last() }
TransactionalDatabaseClient#inTransaction
にわたす関数の中で、データベースへの操作を行っていくMono
を then
などで引き継いで行く(db.execute() 〜
はあくまで操作の予約なので、実際に実行されるのは subscribe
されたあとになる)実行するクエリによっては、この saveUser
関数の中はいるに耐えないものになるので、 Repository
の更に下のレイヤーを作る必要があるように思われる。
以下、ゴミ
なお、前回の続きのようなアプリケーションの続きではこのあたりを抽象的に扱って、次のようなコードになるようにしている。
// サービスクラスの一部 unitOfWorkFactory.unitOfWork( userTransactionalRepository.countUserByName(user.name).condition { it == 0L }, userTransactionalRepository.create(user), tokenTransactionalRepository.create(token)) .thenReturn(user to userToken) // UserTransactionalRepository の一部 RepositoryAction.create { db -> db.insert() .into(UserDb::class.java) .using(UserDb.from(user)) .fetch() .rowsUpdated() }
サンプルコードをそのうちに github にあげるつもりだけど、この休みの後半は時間がほとんど取れなかった…
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
をつける簡単なアプリケーションなので、サービスクラスは省略。コントローラーで頑張る。やった 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()) }
こんな感じで、 r2dbc で RDB につなぐアプリケーションが書ける…
しかし、残念なことに、このアプリケーションはトランザクションを貼っていない(+ コミットしていない)ので、別のインスタンスを立てると、データの状態が同期できないダメダメなアプリになっている…
また、僕たちが作りたいのは複数のテーブルまとめてコミットするかロールバックするアプリケーションなので、これでは足りない…
というわけで、トランザクションを管理できるアプリも作れたのだけど、長いので一旦ここで終わり