mike-neckのブログ

Java or Groovy or Swift or Golang

Spring WebFlux + r2dbc + Kotlin でデータベーストランザクション

f:id:mike_neck:20171113232305p:plain

前回 ReactiveCrudRepository を使ったアプリケーションの話をしたが、残念なことに今の所トランザクションを指定できないので、アプリケーションを終了したら、データはなくなるし、複数のテーブルへの操作をアトミックな操作として指定できないことをメモしておいた。

mike-neck.hatenadiary.com

では、トランザクションをどうおこなうかだが、 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 にわたす関数の中で、データベースへの操作を行っていく
  • 複数のデータベースの操作は前の操作の結果に依存するので(外部参照などの都合による)、失敗した/成功したの状態を引き継ぐために、 Monothen などで引き継いで行く(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 にあげるつもりだけど、この休みの後半は時間がほとんど取れなかった…

クリーンアーキテクチャーの読書メモ(9)

§10 ISP: インターフェース分離の原則

  • 必要としない機能をもつモジュールに依存しない
  • どうしても必要な場合はインターフェースを間に挟む

§11 DIP: 依存関係逆転の原則

  • ソースコードの依存関係が具象ではなく抽象を参照しているのが最も柔軟
  • 安定した抽象に依存、変化しやすい具象に依存しない
    • 変化しやすい具象を参照しない。参照したい場合には AbstractFactory パターンなどで直接参照を避ける
    • 変化しやすい具象クラスを継承しない
      • クラス等の関係性の中で最も強い関係
    • 具象メソッドをオーバーライドしない
      • もとの依存は排除できない
  • インターフェースと実装クラス、 AbstractFactoryFactoryImpl の地点にて依存関係(矢印の向き)が逆転するので依存関係逆転の原則

f:id:mike_neck:20190325010841p:plain
インターフェースの下で依存関係(矢印の向き)が逆転する

Spring WebFlux + r2dbc + Kotlin で簡単なアプリケーションをつくる

5日間休みなので、おさらいも兼ねて、久々に Spring をさわることにしている。なお、 Kotlin にしているのもおさらいを兼ねている。また Kotlin coroutine を使っていないのは r2dbc-postgres と組み合わせたときに、 ByteBufreadableBytes がレスポンス読み出しの直前に 0 になってしまう不具合(?)に遭遇したので、 coroutine を使っていない。 coroutine を使わなければ今のところ、問題なさそう。

f:id:mike_neck:20171113232305p:plain


データベースの設定

データベースの設定は 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 につなぐアプリケーションが書ける…

しかし、残念なことに、このアプリケーションはトランザクションを貼っていない(+ コミットしていない)ので、別のインスタンスを立てると、データの状態が同期できないダメダメなアプリになっている…

また、僕たちが作りたいのは複数のテーブルまとめてコミットするかロールバックするアプリケーションなので、これでは足りない…

というわけで、トランザクションを管理できるアプリも作れたのだけど、長いので一旦ここで終わり

クリーンアーキテクチャーの読書メモ(8)

§9 LSP: リスコフの置換原則

  • 1988年 Barbara Liskov
    • S 型のオブジェクト o1 の各々に、対応する T 型のオブジェクト o2 が 1 つ存在し、 T を使って定義されたプログラム P に対して o2 の代わりに o1 を使っても P の振る舞いが変わらない場合、 S は T の派生型である(『アジャイルソフトウェア開発の奥義』)
  • 正方形・長方形問題
    • リスコフの置換原則を満たしていない
  • リスコフの置換原則は敬称の使い方の指針と考えられていたが、インターフェースと実装に関するソフトウェア設計の原則になっている
  • アーキテクチャーがリスコフの置換原則を満たさない例
    • 業界統一の API パスパターン
    • 一部の企業が API を満たさないように設計する
    • API を呼び出す側はその一部の企業のために if 文をモジュールに追加して呼び出しを調整する(複雑になる)
    • 一部の企業が業界の他の企業を買収する
      • 他の企業の API もあわせることに…
    • API 呼び出し側の複雑性が高くなる

f:id:mike_neck:20190322033333p:plain
リスコフの置換原則を満たしている例

f:id:mike_neck:20190322033634p:plain
正方形・長方形問題