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