mike-neckのブログ

Java or Groovy or Swift or Golang

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
正方形・長方形問題

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

§8 OCP: オープン・クローズドの原則

  • 1988年 Bertrand Meyer
    • ソフトウェアの構成要素は拡張に対しては開いていて、修正に対しては閉じていなければならない(『アジャイルソフトウェア開発の奥義』)
    • = ソフトウェアの振る舞いは既存の成果物を変更せずに拡張できるようにすべき
  • ソフトウェアよりコンポーネントレベルで重大なインパクトを持つ
    • よくある Controller/Service/Model/View/Infraコンポーネント
    • 矢印のしっぽ側のコンポーネントが矢印の向かってる先のコンポーネントを参照(import)している
    • View(ThymeleafView)を変更した際に、 Controller を変更しなくて良い
    • 他のすべてを変更しても Model(BusinessModel)を変更しなくて良い
    • Model が上位のレベルのコンポーネント
    • レベル概念に基づいた変更からの保護階層ができていることが OCP
    • 依存関係逆転の法則を使って、矢印の方向を制御する

f:id:mike_neck:20190320020600p:plain
矢印がすべて向かってくる BusinessModel コンポーネントが最上位レベルのコンポーネント

Hello r2dbc with Kotlin

リアクティブな感じで RDB に接続できるやつ。内容的にはバファさんのブログの下位互換未満。

bufferings.hatenablog.com


準備1

使ったデータベースは postgres で、docker で用意した。とりあえず、こんな感じのテーブルを作っておく。

create table users(
  id bigint not null primary key ,
  name varchar(31) not null unique ,
  created timestamp not null 
);

ついでにデータを入れておく

insert into users (id, name, created)
values (1, 'test-user', '2019-01-01 10:00:00'),
       (2, 'test-admin', '2019-01-15 10:00:00'),
       (3, 'test-owner', '2019-02-01 10:00:00'),
       (4, 'test-guest', '2019-02-15 10:00:00');

準備2

gradle は次のような感じ(Kotlin)

import java.net.URI

plugins {
    id("org.jetbrains.kotlin.jvm").version("1.3.20")
}

repositories {
    jcenter()
    mavenCentral()
    maven {
        url = URI.create("https://repo.spring.io/libs-milestone/")
    }
}
dependencies {
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.1.1")

    implementation("io.projectreactor:reactor-core:3.2.6.RELEASE")
    implementation(group = "io.r2dbc", name = "r2dbc-postgresql", version = "1.0.0.M7")

    testImplementation("org.junit.jupiter:junit-jupiter:5.4.0")
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit5:1.3.21")
    testImplementation("io.projectreactor:reactor-test:3.2.6.RELEASE")
}

ちょっと動かす

テストデータは入っているので、クエリーを投げるところです

@Test
fun noKoroutine() {
  val options = 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()
  val connectionFactory: ConnectionFactory = ConnectionFactories.get(options)
  val conn = Mono.from(connectionFactory.create())
  val users = conn.flatMapMany { connection ->
    connection.createStatement(
        //language=SQL
        "SELECT u.id, u.name, u.created FROM users AS u WHERE u.name = $1")
        .bind("$1", "test-user")
        .execute() }
      .flatMap { result ->
        result.map { row, meta ->
          val name = row.get("name", String::class.javaObjectType)
          val id = row.get("id", Long::class.javaObjectType)
          val created = row.get("created", Instant::class.java)
          return@map if (created != null && name != null && id != null) User(id, name, created)
          else null
        }
      }
      .filter { it != null }
      .cast(User::class.java)
  runBlocking {
    users.buffer().consumeEach {
      println(it)
    }
  }
}

実行結果

f:id:mike_neck:20190317201815p:plain


番外編

Kotlin-Coroutine を使うといい感じのコードになるのかと思ったけど、

// GlobalScope.flux 内
val result = connection.createStatement(
        //language=SQL
        "SELECT u.id, u.name, u.created FROM users AS u WHERE u.name = $1")
        .bind("$1", "test-user")
        .execute()
        .awaitSingle()

result.map { row, _ ->
    val name = row.get("name", String::class.javaObjectType)
    val id = row.get("id", Long::class.javaObjectType)
    val created = row.get("created", Instant::class.java)
    return@map if (created != null && name != null && id != null) User(id, name, created)
    else null
}.consumeEach { if (it != null) channel.offer(it) }

f:id:mike_neck:20190317212450p:plain

AbstractByteBuff#getCharaSequence のあたりで、 ByteBuffreadableBytes0 になってしまう現象に遭遇したので、諦めました

Kotlin Coroutine はもう少し業務寄りの部分で多様なデータが踊っているような箇所で使って、こういう複雑にデータをさわらない箇所ではあまり効果がなさそうに思いました(要検討)。