🦜

Komapper v2.2.0で導入されたCommand機能の紹介

2024/08/17に公開

サーバーサイド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という名前は、@KomapperCommandfunctionNameプロパティで任意の名前に変更できます)。

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

コードの解説

  1. エンティティの定義: Authorエンティティが定義され、authorsテーブルとマッピングされています。@KomapperProjectionは、SQLから取得した結果を注釈したクラスに変換するためのユーティリティ拡張関数(この例ではselectAsAuthor)を生成します。

  2. Commandの定義: CreateAuthor, GetAuthor, GetAuthorBio, ListAuthorsといったCommandクラスが定義されています。各クラスは特定のSQLテンプレートを持ち、クエリの実行結果を処理します。

    • CreateAuthorは、著者を追加し、その結果を返します。ExecReturnOne<Author>を継承しているため、実行結果として追加された著者の情報を返します。
    • GetAuthorは、指定されたIDに対応する著者を取得します。One<Author>を継承しており、単一の結果を返します。
    • GetAuthorBioは、指定されたIDに対応する著者のプロフィールを取得します。One<String>を継承しているため、単一の文字列結果を取得します。
    • ListAuthorsは、全著者のリストを取得します。Many<Author>を継承しているため、複数の結果を返します。
  3. データベース操作: JdbcDatabaseを用いてトランザクションを開始し、各Commandを実行しています。QueryDsl.execute()メソッドを使用することで、事前に定義されたCommandを簡潔に呼び出し、その結果を受け取ることができます。

Command導入の背景

Commandの導入には大きく2つの背景があります。

  1. Domaで実現されているSQLテンプレートのコンパイル時チェックをKomapperでも実現したかった。
  2. 最近、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ベースのアプリケーション開発が一層スムーズに進むでしょう。詳細については、公式ドキュメントをご覧ください。

Komapper公式ドキュメント: COMMANDクエリ

Discussion