🦩

[Kotlin] KomapperとKSP

2022/07/18に公開約8,600字

はじめに

本記事では、Komapperの特徴の1つであるコンパイル時コード生成について紹介します。

KSPとは

Komapperはコンパイル時コード生成を実現するためにKotlin Symbol Processing (KSP) というコンパイラプラグインを使っています。KSPはコンパイル時にソースコードに付与されたアノテーションを読み取って、何らかのチェックを行ったり新しいソースコードを生成したりするツールです。

同じようなツールとしてはkaptがありますが、kaptはすでにメンテナンスモードに入っており、代替としてKSPの利用が推奨されています。KSPもkaptもコンパイル時にコードに付与されたアノテーションを読み取って何らかの処理を実行できるという点は同じですが、KSPの方がより効率的な作りになっています。

kaptはJavaのアノテーションプロセッサの仕様であるJSR 269を前提としたアーキテクチャとなっており、KotlinコードをJavaコード(スタブ)に変換してからJSR 269のアノテーションプロセッシングを実行します。変換が必要な分だけ処理が遅いという問題がありました。KSPはJSR 269は使わずKotlinのコードを直接解釈するアプローチをとっておりこの問題を解決しています。

さらに、「エラーの発生箇所がわかりにくい」というkaptの問題もKSPは解決できているように思われます。kaptを介したアノテーションプロセッシングは、エラーの発生箇所として、オリジナルのKotlinのコード位置ではなく変換後のJavaのコード位置をレポートします。一方で、KSPはオリジナルのKotlinのコード位置をレポートするのでユーザーにとって直感的です。

Komapperにおけるコード生成

Koampperでは、独自のアノテーションを使って次のようにエンティティを定義します。

@KomapperEntity
data class Person(
    @KomapperId @KomapperAutoIncrement
    val id: Int = 0,
    val name: String
)

上記のコードをコンパイルするとこのエンティティ定義の詳細を保持するクラス(メタモデルと呼んでいます)のソースコードが出力されます。メタモデルは例えば次のような情報を保持します。

  • マッピング対象のテーブル名
  • 永続対象プロパティ
  • プライマリキーに対応するプロパティ
  • プライマリキーの値を生成する方法
  • エンティティインスタンスを生成する方法

メモモデルのコードに興味がある方は、下記のアコーディオンを展開してみてください(長ったらしいので隠しています)。

メタモデルのコード(_Person.kt)
@file:Suppress("ClassName", "PrivatePropertyName", "UNUSED_PARAMETER", "unused", "RemoveRedundantQualifierName", "MemberVisibilityCanBePrivate", "RedundantNullableReturnType", "USELESS_CAST", "UNCHECKED_CAST", "RemoveRedundantBackticks")
package org.komapper.quickstart

// generated at 2022-07-18T10:35:40.923365+09:00[Asia/Tokyo]
@org.komapper.core.dsl.metamodel.EntityMetamodelImplementor(org.komapper.quickstart.Person::class)
class _Person private constructor(table: String = "person", catalog: String = "", schema: String = "", alwaysQuote: Boolean = false, disableSequenceAssignment: Boolean = false, declaration: org.komapper.core.dsl.metamodel.EntityMetamodelDeclaration<_Person> = {}) : org.komapper.core.dsl.metamodel.EntityMetamodel<org.komapper.quickstart.Person, kotlin.Int, _Person> {
    private val __tableName = table
    private val __catalogName = catalog
    private val __schemaName = schema
    private val __alwaysQuote = alwaysQuote
    private val __disableSequenceAssignment = disableSequenceAssignment
    private val __declaration = declaration
    private object __EntityDescriptor {
        val `id` = org.komapper.core.dsl.metamodel.PropertyDescriptor<org.komapper.quickstart.Person, kotlin.Int, kotlin.Int>(kotlin.Int::class, kotlin.Int::class, "id", "id", false, false, { it.`id` }, { e, v -> e.copy(`id` = v) }, { it }, { it }, false)
        val `name` = org.komapper.core.dsl.metamodel.PropertyDescriptor<org.komapper.quickstart.Person, kotlin.String, kotlin.String>(kotlin.String::class, kotlin.String::class, "name", "name", false, false, { it.`name` }, { e, v -> e.copy(`name` = v) }, { it }, { it }, false)
    }
    val `id`: org.komapper.core.dsl.metamodel.PropertyMetamodel<org.komapper.quickstart.Person, kotlin.Int, kotlin.Int> by lazy { org.komapper.core.dsl.metamodel.PropertyMetamodelImpl(this, __EntityDescriptor.`id`) }
    val `name`: org.komapper.core.dsl.metamodel.PropertyMetamodel<org.komapper.quickstart.Person, kotlin.String, kotlin.String> by lazy { org.komapper.core.dsl.metamodel.PropertyMetamodelImpl(this, __EntityDescriptor.`name`) }
    override fun klass() = org.komapper.quickstart.Person::class
    override fun tableName() = __tableName
    override fun catalogName() = __catalogName
    override fun schemaName() = __schemaName
    override fun alwaysQuote() = __alwaysQuote
    override fun disableSequenceAssignment() = __disableSequenceAssignment
    override fun declaration() = __declaration
    override fun idGenerator(): org.komapper.core.dsl.metamodel.IdGenerator<org.komapper.quickstart.Person, kotlin.Int>? = org.komapper.core.dsl.metamodel.IdGenerator.AutoIncrement(`id`)
    override fun idProperties(): List<org.komapper.core.dsl.metamodel.PropertyMetamodel<org.komapper.quickstart.Person, *, *>> = listOf(`id`)
    override fun versionProperty(): org.komapper.core.dsl.metamodel.PropertyMetamodel<org.komapper.quickstart.Person, *, *>? = null
    override fun createdAtProperty(): org.komapper.core.dsl.metamodel.PropertyMetamodel<org.komapper.quickstart.Person, *, *>? = null
    override fun updatedAtProperty(): org.komapper.core.dsl.metamodel.PropertyMetamodel<org.komapper.quickstart.Person, *, *>? = null
    override fun properties(): List<org.komapper.core.dsl.metamodel.PropertyMetamodel<org.komapper.quickstart.Person, *, *>> = listOf(
        `id`,
        `name`)
    override fun extractId(e: org.komapper.quickstart.Person): kotlin.Int = e.`id`
    override fun convertToId(generatedKey: Long): kotlin.Int? = this.`id`.wrap(generatedKey.toInt())
    override fun versionAssignment(): Pair<org.komapper.core.dsl.metamodel.PropertyMetamodel<org.komapper.quickstart.Person, *, *>, org.komapper.core.dsl.expression.Operand>? = null
    override fun createdAtAssignment(c: java.time.Clock): Pair<org.komapper.core.dsl.metamodel.PropertyMetamodel<org.komapper.quickstart.Person, *, *>, org.komapper.core.dsl.expression.Operand>? = null
    override fun updatedAtAssignment(c: java.time.Clock): Pair<org.komapper.core.dsl.metamodel.PropertyMetamodel<org.komapper.quickstart.Person, *, *>, org.komapper.core.dsl.expression.Operand>? = null
    override fun preInsert(e: org.komapper.quickstart.Person, c: java.time.Clock): org.komapper.quickstart.Person = e
    override fun preUpdate(e: org.komapper.quickstart.Person, c: java.time.Clock): org.komapper.quickstart.Person = e
    override fun postUpdate(e: org.komapper.quickstart.Person): org.komapper.quickstart.Person = e
    override fun newEntity(m: Map<org.komapper.core.dsl.metamodel.PropertyMetamodel<*, *, *>, Any?>) = org.komapper.quickstart.Person(
        `id` = m[this.`id`] as kotlin.Int,
        `name` = m[this.`name`] as kotlin.String)
    override fun newMetamodel(table: String, catalog: String, schema: String, alwaysQuote: Boolean, disableSequenceAssignment: Boolean, declaration: org.komapper.core.dsl.metamodel.EntityMetamodelDeclaration<_Person>) = _Person(table, catalog, schema, alwaysQuote, disableSequenceAssignment, declaration)
    fun clone(table: String = "person", catalog: String = "", schema: String = "", alwaysQuote: Boolean = false, disableSequenceAssignment: Boolean = false, declaration: org.komapper.core.dsl.metamodel.EntityMetamodelDeclaration<_Person> = {}) = _Person(table, catalog, schema, alwaysQuote, disableSequenceAssignment, declaration)
    companion object {
        init {
            org.komapper.core.dsl.metamodel.checkMetamodelVersion("org.komapper.quickstart._Person", 1)
        }
        val `person` = _Person()
    }
}

val org.komapper.core.dsl.Meta.`person` get() = _Person.`person`

コンパイル時コード生成のメリット

コンパイル時にメタモデルを生成するメリットは何でしょうか?Komapperにおいて代表的なものを2つ挙げます。

  1. リフレクションを避けられる
  2. メタモデルを利用して簡潔にコードが書ける

リフレクションを避けられる

エンティティ定義の詳細を取得する別の方法として、実行時にリフレクションを用いる方法があります。しかし、リフレクションにはいくつかデメリットがあります。

  1. 実行時エラーが増える(エンティティ定義に不備がある場合など実行時に判明する)
  2. リフレクションで取得された情報を確認するのが手間である(デバッガーやログを活用する必要がある)
  3. GraalVMを使ってネイティブイメージに変換するために追加の設定が必要である

コンパイル時のコード生成ではこれらの問題は起きません。1についてはコンパイル時に設定不備に気付けます。2については生成されるメタモデルのコードを目で見て確認できます。3については追加の設定が不要です。

メタモデルを利用して簡潔にコードが書ける

Komapperでは、メタモデルを利用して型の恩恵を受けつつSQLクエリを組み立てることができます。例えば、上述のPersonに対応するメタモデルを使った例は以下の通りです。

// メタモデルのインスタンスを取得
val p = Meta.person
// クエリを定義
val query = QueryDsl.from(p).where { p.name greaterEq "K" }.orderBy(p.id)

p.name greaterEq "K"の箇所ですが、もし"K"という文字列ではなく別の型の値を渡したらコンパイルエラーになります。p.nameがString型専用のプロパティだからです。

上述のqueryを実行すると以下のようなSQLに変換されます。

select t0_.id, t0_.name from person as t0_ where t0_.name >= ? order by t0_.id asc

おまけ - KSP雑感

KSPの利用者として少し感想を述べてみたいと思います。

KSPはJSR 269のAPIを使ったことがある人なら直感的に使えると思います。全部ではないですが、大体似たようなAPIがあります。KSPはKotlin独自の言語機能(例えばdata classやvalue class)を判別できるので、JSR 269よりも応用範囲が広いと感じています。

もしかするとKSPの主要なターゲットはAndroidかもしれませんが、Android特化の何かがあるわけではないのでサーバサイド向けのKomapperのようなライブラリでも全く問題なく利用できます。

KSPの開発はGoogleの人を中心に活発です。KotlinのSlackに専用のチャネル(#ksp)もあり、質問の流通量も結構あります。

おわりに

Komapperのコンパイル時コード生成機能を紹介しました。もし興味があればクイックスタートなどお試しください。

https://www.komapper.org/ja/docs/quickstart/

参考情報

KSPのドキュメント

https://kotlinlang.org/docs/ksp-overview.html

日本語によるKSPの解説動画

https://www.youtube.com/watch?v=sJfURbrdau0

Discussion

ログインするとコメントできます