🙌

[Kotlin/JVM] SQLを書くのが好きな人のための Kuery Client の紹介

2024/07/02に公開

はじめに

どんなふうに書けるの?

前置きから書きたいところですが、まずは雰囲気だけ紹介します。

suspend fun search(status: String, vip: Boolean?): List<User> = kueryClient
    .sql {
        +"""
        SELECT * FROM users
        WHERE
        status = $status
        """
        if (vip != null) {
            +"vip = $vip"
        }
    }
    .list()
  • +(unaryPlus)を使って、SQLを連結して組み立てることができます
    • 動的にSQLを構築したいときは、if等を使ってください
    • (もちろん、動的に組み立てる必要がないなら、ヒアドキュメントでベタ書きすればいいです)
  • 動的な値を埋め込みたいときは、文字列補間を利用してください
    • そんなことしたらSQL Injectionになるのでは...と当然思うかもしれませんが、kotlin compiler pluginを実装することで、プレースホルダーとして評価するようになっています

モチベーション

元々は、SQLを手で書けるMyBatisが好き

(JVMにて)世の中には多くのデータベースクライアントライブラリがありますが、自分はMyBatisを好んで使っていました。

好んでいる理由としてはざっくりと以下の通りです。

  • SQLを直接書きたい
    • 大規模な環境で利用しているため、少しくらい手間がかかってでもいいのでSQLを直接書きたい
    • SQLを書いておくとExplain等を実施しやすいため
    • 稀にIndex Hintを指定したいケースもあり、こういったケースにもサっと対応できる
  • ライブラリ独自の記法やDSLを学習したくない
      - SQLさえ知っていれば十分、程度だと嬉しい

SQLを直接書くことにより特定のデータベースに依存することにはなりますが、データベースを移行(例: MySQLからPostgreSQLに移行する)というケースはそもそもないですし、仮にそういった機会が訪れるとしたらどちらにせよ時間をかけて入念に検証をするはず(これと比べたらSQLを直すことはそこまで手間ではない)なので、これに関してはあまりデメリットと捉えていません。

MyBatisがR2DBC対応をしていない

ここ最近、業務でSpring WebFlux & Coroutinesを使ってアプリケーションを書く機会が増えてきたのですが、そうなってくるとR2DBC(https://r2dbc.io/)を使用したくなってきます。
(以前は JDBCの呼び出し部分だけ withContext(Dispatchers.IO) {...} をしていました )

残念ながら前述したMyBatisはR2DBC対応をしていません。

SQLを手で書けるR2DBCライブラリというと...

既存のR2DBC対応をしたライブラリはいわゆる独自の記法/DSLを使うものが多く、前述した自分の好みにマッチするものがありませんでした。

一方で、こういったDSLはtype safeになるというメリットはあります。integerのカラムに間違えて文字列をいれるという間違いがなくなります。

が...、個人的にはSQLを書いたら当然それに対応するunit testも書くため、これに関してはどちらでもいいかなと思っています。

(おまけ) sqlc と SQLDelight

近頃話題になったsqlcは自分の好みにマッチしているのでとても良さそうです。(gRPCっぽい発想で好き)

が。kotlin対応もしているようですが、残念ながらJDBC対応のみです。

生成するコード等を見るとわかりますが、sqlcのgo対応と比較すると最低限の実装しかまだないようです。

また、Kotlinの場合、SQLDelightというsqlcに似たものがあります。これはR2DBC対応を謳ってはいるのですが、なぜか引数がConnectionFactoryになっていないのでconnection poolを使いにくかったりTransactionサポートが甘かったりしてまだまだ発展途上といった印象を受けます。

また、どちらも動的にクエリを構築することはできないようです。(こういった動的なクエリに関しては是非の意見があるとは思いますが)
https://github.com/cashapp/sqldelight/issues/4836

文字列テンプレート & 文字列補間で書きたい

また、手でSQLを書くのであれば、Scalaで人気があるDoobieのように文字列テンプレート & 文字列補間で書きたい気もします。

def find(n: String): ConnectionIO[Option[Country]] =
  sql"select code, name, population from country where name = $n".query[Country].option

ただし残念ながら、Kotlinでは文字列補間の挙動をカスタマイズすることができません。
(Javaだと最近はできるようになったのに...)

ちなみに、これについてはこのあたりで議論されています。

そんな中、Kotlin Compiler Pluginを知る

自分は元々はKotlin/JVMをメインにしていたのですが、近頃はKMP(Kotlin Multiplatform)にハマりこんでいます。
(動機は単純で...全部Kotlinで書けたら便利なので...)

すると、KMP向けのライブラリだとわりとKotlin Compiler Plugin込みで機能提供しているライブラリが多いことに気づきます。
(Kotlin Serializationが最たる例)

noarg pluginやらallopen pluginなどは元々使っていましたが、サードパーティでも作っている人がいるのか、という気付きを得ます。

もしかしてこれ使えば文字列補間の挙動を特定のメソッドだけ変えることができるのでは...?とうっすら思い始めます。そんな中、Kotlin Fest 2024の以下の発表のスライドを目にしました。(当日、自分は予定があったので不参加だったのでしたが...)

https://speakerdeck.com/kitakkun/kotlin-fest-2024-motutokotlinwohao-kininaru-k2shi-dai-nokotlin-compiler-pluginkai-fa

結果、冒頭のような感じで使えるSQLクライアントができました。

Kuery Clientの使い方

他のライブラリと同様に、Gradleの依存に足すだけです。
ただし、Kotlin Compiler Pluginが必要なので、kuery clientが提供するGradle Pluginも利用してください。

plugins {
    id("dev.hsbrysk.kuery-client") version "{{version}}"
}

implementation("dev.hsbrysk.kuery-client:kuery-client-spring-data-r2dbc:{{version

Kuery Clientの特徴

Builder と 文字列補間

Kotlinでは buildStringbuildList に代表されるように、Builder Scopeを作り、そこで動的に構築していくようなスタイルが採用されるケースが多いです。

buildString {
    append("hoge")
    if (...) {
        append("bar")
    }
}

kotlinx.htmlでも、この書き方を採用しています。
+ を使ってtext nodeを追加しています。

val body = document.body ?: error("No body")
body.append {
    div {
        p {
            +"Welcome!"
            +"Here is "
            a("https://kotlinlang.org") { +"official Kotlin site" }
        }
    }
}

この書き方を踏襲し、冒頭のようなスタイルを使うようにしています。(再掲)

suspend fun search(status: String, vip: Boolean?): List<User> = kueryClient
    .sql {
        +"""
        SELECT * FROM users
        WHERE
        status = $status
        """
        if (vip != null) {
            +"vip = $vip"
        }
    }
    .list()

Based on spring-data-r2dbc or spring-data-jdbc

現時点では、広く使われていて実績があるこれらを基に実装しています。なので、R2DBC/JDBC両方で使うことができます。
(元々はR2DBC向けに作りたかったのがきっかけでしたが、せっかくなのでJDBCにも対応しています)

Kuery Clientの要件のためにこれらに依存するのは若干too muchではあるのですが、transaction supportやらtype conversionなりの対応をそのまま使うことができるので...。

(気が向いたら、これらに依存しないmoduleも作るかもしれません)

Transaction

SpringのTransactionサポートをそのまま使うことができます。

詳細はこちらをご覧ください。

https://kuery-client.hsbrysk.dev/transaction.html

Observation

Micrometer Observationをサポートしているので、MetricsもTracingにも対応しています。

詳細はこちらをご覧ください。

https://kuery-client.hsbrysk.dev/observation.html

Type Conversion

spring-dataベースなので、springのtype conversionを使ってください。
独自の型があっても、柔軟に対応できます。特定の型だけ暗号化したい等にも対応できるはずです。

詳細はこちらをご覧ください。

https://kuery-client.hsbrysk.dev/type-conversion.html

Detekt Custom Rule

以下のような間違えた書き方をすると、SQL Injection等が起きる可能性があります。

kueryClient.sql {
    // BAD !!
    val sql = "SELECT * FROM user WHERE id = $id"
    +sql
}

文字列補間をカスタマイズしているのはKuery Clientの特定のメソッドだけなので、こういった書き方は駄目です。
こういった間違った書き方を検出するためのDetekt Custom Ruleを提供しています。

https://kuery-client.hsbrysk.dev/detekt.html

Exampleコード

Spring Bootと組み合わせたサンプルコードです。

おわりに

より詳しく知りたい人は、ドキュメントサイトを見てみてください。(といっても、現状めちゃくちゃ簡素なんですが...)
https://kuery-client.hsbrysk.dev/

さっそく個人プロダクトで使ってみていますが、なかなか使いやすくて気に入っています。

今後の展望としては、せっかくSQLを手で書いているので、SQL系のlinter類と統合していきたいかなーとぼんやり思っています。

あとは、ScalaのDoobieだとクエリの型チェックがあるようなのですが、これと似たようなものも実現してみたいです。(あまりちゃんと使ったことはないのでまだ詳しくはないのですが)
https://tpolecat.github.io/doobie/docs/06-Checking.html

前述したモチベーションではこのように書きはしましたが、堅牢にできるならそれに越したことはないでしょう。

一方で、DSLのおかげでtype safeになるというメリットはあります。integerのカラムに間違えて文字列をいれるという間違いがなくなります。
が...、個人的にはSQLを書いたら当然それに対応するunit testも書くため、これに関してはどちらでもいいかなと思っています。

Discussion