Komapper v2.2.0で導入されたCommand機能の紹介
サーバーサイドKotlin向けのORMであるKomapper v2.2.0では、「Command」という新しい概念が導入されました。この機能により、SQLテンプレートを用いたクエリの作成がより便利になりました。
Commandの概要
Commandは、SQLテンプレート、パラメータ、SQLの実行結果のハンドラーを1つにまとめたものです。例えば、複数件を取得するコマンドは次のように記述できます。
@KomapperCommand("""
select * from ADDRESS where street = /*street*/'test'
""")
class ListAddresses(val street: String): Many<Address>({ selectAsAddress() })
ここでのポイントをいくつか挙げます。
- クラスに
@KomapperCommand
を付与します。 -
@KomapperCommand
にはSQLテンプレートを記述します。 - クラスのコンストラクタでSQLテンプレートのパラメータを定義します。
-
Many
を継承することで複数件の取得であることを示します。 -
Many
の型パラメータには複数件の要素を表す型を指定します(List
に型パラメータを指定するのと同様です)。 -
Many
のコンストラクタにラムダ式を渡し、結果の処理方法を指定します。
このコマンドをビルドすると、コンパイル時に以下の処理が行われます。
- SQLテンプレートの構文チェック。例えば、
/*%if ... */
に対応する/*%end */
が存在するか確認し、存在しない場合はエラーとします。 - パラメータがSQLテンプレート内で利用されているかのチェック。利用されていない場合は警告メッセージを出力します。
- テンプレート内でパラメータのメンバ(プロパティや関数)にアクセスしている場合、そのメンバの存在チェック。存在しない場合はエラーとします。
- クエリを組み立てるためのAPIである
QueryDsl
に、execute
という名前の拡張関数を生成。execute
は@KomapperCommand
が注釈されたクラスをパラメータとして受け取ります(execute
という名前は、@KomapperCommand
のfunctionName
プロパティで任意の名前に変更できます)。
Commandの種類
Commandには5つの種類があります。
- One: 1件を取得する。
- Many: 複数件を取得する。
- Exec: 更新系DMLを実行し、影響を受けた件数を返す。
- ExecReturnOne: 更新系DMLを実行し、1件を取得する。
- ExecReturnMany: 更新系DMLを実行し、複数件を取得する。
これらはいずれも、@KomapperCommand
が付与されたクラスから継承することで示されます。
サンプルコード
以下は、TestcontainersのPostgreSQLにアクセスして、いくつかのCommandを実行する例です。
package org.komapper.quickstart
import org.komapper.annotation.*
import org.komapper.core.ExecReturnOne
import org.komapper.core.Many
import org.komapper.core.One
import org.komapper.core.dsl.Meta
import org.komapper.core.dsl.QueryDsl
import org.komapper.core.dsl.query.getNotNull
import org.komapper.core.dsl.query.single
import org.komapper.jdbc.JdbcDatabase
@KomapperEntity
@KomapperProjection
@KomapperTable("authors")
data class Author(
@KomapperId
@KomapperAutoIncrement
val id: Int,
val name: String,
val bio: String,
)
@KomapperCommand(
"""
INSERT INTO authors (
name, bio
) VALUES (
/* name */'', /* bio */''
)
RETURNING *;
"""
)
class CreateAuthor(val name: String, val bio: String) : ExecReturnOne<Author>({ selectAsAuthor().single() })
@KomapperCommand(
"""
SELECT * FROM authors
WHERE id = /* id */0 LIMIT 1;
"""
)
class GetAuthor(val id: Int) : One<Author>({ selectAsAuthor().single() })
@KomapperCommand(
"""
SELECT bio FROM authors
WHERE id = /* id */0 LIMIT 1;
"""
)
class GetAuthorBio(val id: Int) : One<String>({ select { it.getNotNull<String>(0) }.single() })
@KomapperCommand(
"""
SELECT * FROM authors
"""
)
class ListAuthors : Many<Author>({ selectAsAuthor() })
fun main() {
val db = JdbcDatabase("jdbc:tc:postgresql:///test")
db.withTransaction {
// create schema
db.runQuery {
QueryDsl.create(Meta.author)
}
// create an author and get the author
val alice = db.runQuery {
QueryDsl.execute(CreateAuthor("Alice", "Alice's bio"))
}
println("alice: $alice")
// create an author and get the author
val bob = db.runQuery {
QueryDsl.execute(CreateAuthor("Bob", "Bob's bio"))
}
println("bob: $bob")
// list all authors
val authors = db.runQuery {
QueryDsl.execute(ListAuthors())
}
println("authors: $authors")
// get an author
val one = db.runQuery {
QueryDsl.execute(GetAuthor(alice.id))
}
println("one: $one")
// get author's bio
val bio = db.runQuery {
QueryDsl.execute(GetAuthorBio(alice.id))
}
println("bio: $bio")
}
}
上記のコードを実行すると次の文字列が出力されます。
alice: Author(id=1, name=Alice, bio=Alice's bio)
bob: Author(id=2, name=Bob, bio=Bob's bio)
authors: [Author(id=1, name=Alice, bio=Alice's bio), Author(id=2, name=Bob, bio=Bob's bio)]
one: Author(id=1, name=Alice, bio=Alice's bio)
bio: Alice's bio
コードの解説
-
エンティティの定義:
Author
エンティティが定義され、authors
テーブルとマッピングされています。@KomapperProjection
は、SQLから取得した結果を注釈したクラスに変換するためのユーティリティ拡張関数(この例ではselectAsAuthor
)を生成します。 -
Commandの定義:
CreateAuthor
,GetAuthor
,GetAuthorBio
,ListAuthors
といったCommandクラスが定義されています。各クラスは特定のSQLテンプレートを持ち、クエリの実行結果を処理します。-
CreateAuthor
は、著者を追加し、その結果を返します。ExecReturnOne<Author>
を継承しているため、実行結果として追加された著者の情報を返します。 -
GetAuthor
は、指定されたIDに対応する著者を取得します。One<Author>
を継承しており、単一の結果を返します。 -
GetAuthorBio
は、指定されたIDに対応する著者のプロフィールを取得します。One<String>
を継承しているため、単一の文字列結果を取得します。 -
ListAuthors
は、全著者のリストを取得します。Many<Author>
を継承しているため、複数の結果を返します。
-
-
データベース操作:
JdbcDatabase
を用いてトランザクションを開始し、各Commandを実行しています。QueryDsl.execute()
メソッドを使用することで、事前に定義されたCommandを簡潔に呼び出し、その結果を受け取ることができます。
Command導入の背景
Commandの導入には大きく2つの背景があります。
- Domaで実現されているSQLテンプレートのコンパイル時チェックをKomapperでも実現したかった。
- 最近、sqlcの評判をよく耳にするようになり、sqlcに似た要素を持ちつつも、Komapperと相性の良いAPIを実現したいと考えた。
sqlcとの違い
前提条件やアプローチ方法が異なるため単純な比較はできませんが、ここではsqlcとの違いについて簡単に触れてみます。
sqlcと異なり、KomapperはSQLではなくテンプレート文字列をパースします。そのため、テンプレート構文を利用して条件分岐やループなどを実現できます。例えば、WHERE句が動的に切り替わるようなSQL文字列への変換が容易です。
また、テンプレートを使用することで、SQLだけでは表現できない機能を実現できます。例えば、この記事では触れていませんが、再利用可能な他のテンプレートを取り込むこともサポートしています(興味があれば、ドキュメントのパーシャルをご参照ください)。
他の利点として、特定のDBMSに特化していないため、さまざまなDBMSと組み合わせて利用できることが挙げられます。
しかし、逆に言えば、KomapperはSQLの構文を完全には認識しないため、SQLの構文の正しさを検証したり、SQLを他の同等のSQLに変換したりすることはできません。例えば、sqlcではサポートされていると思われますが、SELECT *
の*
を自動展開する機能はありません。
Komapperでは、SQLの構文の正しさはpgAdminなどのツールや単体テストを通じて確認することを想定しています。*
の自動展開は、Domaで実現されているように、SQLテンプレートでSELECT /*%expand */*
のような記述をサポートすれば実現可能ですが、現時点では未サポートです。
まとめ
Komapper v2.2.0で導入されたCommandは、SQLテンプレートをKotlinコードに統合し、クエリの作成と実行をより直感的かつ簡潔に行える機能です。SQLの複雑さをKotlinのシンプルな構文に組み込むことで、データベース操作がさらに効率的に行えるようになります。この新機能を活用することで、Kotlinベースのアプリケーション開発が一層スムーズに進むでしょう。詳細については、公式ドキュメントをご覧ください。
Discussion