TEMPLATEクエリ

SQLテンプレートを利用するクエリ

概要

TEMPLATEクエリはSQLテンプレートを利用して構築するクエリです。

TEMPLATEクエリはコアのモジュールには含まれないオプション機能です。 利用するにはGradleの依存関係に次のような宣言が必要です。

val komapperVersion: String by project
dependencies {
    implementation("org.komapper:komapper-template:$komapperVersion")
}

fromTemplate

検索を実施するにはfromTemplate関数に SQLテンプレートbind関数にデータを渡します。 検索結果を任意の型に変換するために、select関数にラムダ式を渡します。

val sql = "select * from ADDRESS where street = /*street*/'test'"
val query: Query<List<Address>> = QueryDsl.fromTemplate(sql)
    .bind("street", "STREET 10")
    .select { row: Row -> 
        Address(
            row.getNotNull("address_id"),
            row.getNotNull("street"),
            row.getNotNull("version")
        )
}

select関数に渡すラムダ式に登場するRowjava.sql.ResultSetio.r2dbc.spi.Rowの薄いラッパーです。 カラムのラベル名やインデックスで値を取得する関数を持ちます。 なお、インデックスは0から始まります。

options

クエリの挙動をカスタマイズするにはoptionsを呼び出します。 ラムダ式のパラメータはデフォルトのオプションを表します。 変更したいプロパティを指定してcopyメソッドを呼び出してください。

val sql = "select * from ADDRESS where street = /*street*/'test'"
val query: Query<List<Address>> = QueryDsl.fromTemplate(sql)
  .options {
    it.copy(
      fetchSize = 100,
      queryTimeoutSeconds = 5
    )
  }
  .bind("street", "STREET 10")
  .select { row: Row ->
    Address(
      row.getNotNull("address_id"),
      row.getNotNull("street"),
      row.getNotNull("version")
    )
}

指定可能なオプションには以下のものがあります。

escapeSequence
LIKE句に指定されるエスケープシーケンスです。デフォルトはnullDialectの値を使うことを示します。
fetchSize
フェッチサイズです。デフォルトはnullでドライバの値を使うことを示します。
maxRows
最大行数です。デフォルトはnullでドライバの値を使うことを示します。
queryTimeoutSeconds
クエリタイムアウトの秒数です。デフォルトはnullでドライバの値を使うことを示します。
suppressLogging
SQLのログ出力を抑制するかどうかです。デフォルトはfalseです。

executionOptions の同名プロパティよりもこちらに明示的に設定した値が優先的に利用されます。

executeTemplate

更新系のDMLを実行するにはexecuteTemplate関数に SQLテンプレートbind関数にデータを渡します。

クエリ実行時にキーが重複した場合、org.komapper.core.UniqueConstraintExceptionがスローされます。

val sql = "update ADDRESS set street = /*street*/'' where address_id = /*id*/0"
val query: Query<Long> = QueryDsl.executeTemplate(sql)
    .bind("id", 15)
    .bind("street", "NY street")

options

クエリの挙動をカスタマイズするにはoptionsを呼び出します。 ラムダ式のパラメータはデフォルトのオプションを表します。 変更したいプロパティを指定してcopyメソッドを呼び出してください。

val sql = "update ADDRESS set street = /*street*/'' where address_id = /*id*/0"
val query = Query<Long> = QueryDsl.executeTemplate(sql)
  .bind("id", 15)
  .bind("street", "NY street")
  .options {
    it.copy(
      queryTimeoutSeconds = 5
    )
}

指定可能なオプションには以下のものがあります。

escapeSequence
LIKE句に指定されるエスケープシーケンスです。デフォルトはnullDialectの値を使うことを示します。
queryTimeoutSeconds
クエリタイムアウトの秒数です。デフォルトはnullでドライバの値を使うことを示します。
suppressLogging
SQLのログ出力を抑制するかどうかです。デフォルトはfalseです。

executionOptions の同名プロパティよりもこちらに明示的に設定した値が優先的に利用されます。

SQLテンプレート

Komapperが提供するSQLテンプレートはいわゆる2-Way-SQL対応のテンプレートです。 バインド変数や条件分岐に関する記述をSQLコメントで表現するため、 テンプレートをアプリケーションで利用できるだけでなく、pgAdmin など一般的なSQLツールでも実行できます。

例えば条件分岐とバインド変数を含んだSQLテンプレートは次のようになります。

select name, age from person where
/*%if name != null*/
  name = /*name*/'test'
/*%end*/
order by name

上記のテンプレートはアプリケーション上でname != nullが真と評価されるとき次のSQLに変換されます。

select name, age from person where name = ? order by name

name != nullが偽と評価されるとき次のSQLに変換されます。

select name, age from person order by name

バインド変数ディレクティブ

バインド変数は/*expression*/のように/**/で囲んで表します。 expressionには任意の値を返す式が入ります。

次の'test'のようにディレクティブの直後にはテスト用の値が必須です。

where name = /*name*/'test'

最終的にはテスト用の値は取り除かれ上述のテンプレートは次のようなSQLに変換されます。 /*name*/?に置換され、?にはnameが返す値がバインドされます。

where name = ?

IN句にバインドするにはexpressionIterable型の値でなければいけません。

where name in /*names*/('a', 'b')

IN句にタプル形式で値をバインドするにはexpressionIterable<Pair>型やIterable<Triple>型の値にします。

where (name, age) in /*pairs*/(('a', 'b'), ('c', 'd'))

リテラル変数ディレクティブ

リテラル変数は/*^expression*/のように/*^*/で囲んで表します。 expressionには任意の値を返す式が入ります。

次の'test'のようにディレクティブの直後にはテスト用の値が必須です。

where name = /*^name*/'test'

最終的にはテスト用の値は取り除かれ上述のテンプレートは次のようなSQLに変換されます。 /*^name*/nameが返す値(この例では"abc")のリテラル表現('abc')で置換されます。

where name = 'abc'

埋め込み変数ディレクティブ

埋め込み変数は/*#expression*/のように/*#*/で囲んで表します。 expressionには任意の値を返す式が入ります。

select name, age from person where age > 1 /*# orderBy */

上述のorderByの式が"order by name"という文字列を返す場合、最終的なSQLは次のように変換されます。

select name, age from person where age > 1 order by name

ifディレクティブ

ifの条件分岐は/*%if expression*/で始めて/*%end*/で終わります。 expressionには真偽値を返す式が入ります。

/*%if name != null*/
  name = /*name*/'test'
/*%end*/

/*%if expression*//*%end*/の間に/*%else*/を入れることもできます。

/*%if name != null*/
  name = /*name*/'test'
/*%else*/
  name is null
/*%end*/

forディレクティブ

forを使ったループは/*%for identifier in expression */で始めて/*%end*/で終わります。 expressionにはIterableを返す式が入りidentifierIterableのそれぞれの要素を表す識別子となります。 forのループの中ではidentifier_has_nextのプレフィックをつけた識別子が利用可能になります。 これは次の繰り返し要素が存在するかどうかを表す真偽値を返します。

/*%for name in names */
employee_name like /* name */'hoge'
  /*%if name_has_next */
/*# "or" */
  /*%end */
/*%end*/

endディレクティブ

条件分岐やループ処理を終了するには、endディレクティブを使います。

endディレクティブは/*%end*/というSQLコメントで表現されます。

ディレクティブ内で参照される式の中では以下の機能がサポートされています。

  • 演算子の実行
  • プロパティアクセス
  • 関数呼び出し
  • クラス参照
  • 拡張プロパティや拡張関数の利用

演算子

次の演算子がサポートされています。意味はKotlinの演算子と同じです。

  • ==
  • !=
  • >=
  • <=
  • >
  • <
  • !
  • &&
  • ||

次のように利用できます。

/*%if name != null && name.length > 0 */
  name = /*name*/'test'
/*%else*/
  name is null
/*%end*/

プロパティアクセス

.?.を使ってプロパティにアクセスできます。?.はKotlinのsafe call operatorと同じ挙動をします。

/*%if person?.name != null */
  name = /*person?.name*/'test'
/*%else*/
  name is null
/*%end*/

関数呼び出し

関数を呼び出せます。

/*%if isValid(name) */
  name = /*name*/'test'
/*%else*/
  name is null
/*%end*/

クラス参照

@クラスの完全修飾名@という記法でクラスを参照できます。 例えばexample.Directionというenum classにWESTという要素がある場合、次のように参照できます。

/*%if direction == @example.Direction@.WEST */
  direction = 'west'
/*%end*/

拡張プロパティと拡張関数

Kotlinが提供する以下の拡張プロパティと拡張関数をデフォルトで利用できます。

  • val CharSequence.lastIndex: Int
  • fun CharSequence.isBlank(): Boolean
  • fun CharSequence.isNotBlank(): Boolean
  • fun CharSequence.isNullOrBlank(): Boolean
  • fun CharSequence.isEmpty(): Boolean
  • fun CharSequence.isNotEmpty(): Boolean
  • fun CharSequence.isNullOrEmpty(): Boolean
  • fun CharSequence.any(): Boolean
  • fun CharSequence.none(): Boolean
/*%if name.isNotBlank() */
  name = /*name*/'test'
/*%else*/
  name is null
/*%end*/

また、Komapperが定義する以下の拡張関数も利用できます。

  • fun String?.asPrefix(): String?
  • fun String?.asInfix(): String?
  • fun String?.asSuffix(): String?
  • fun String?.escape(): String?

例えば、asPrefix()を呼び出すと"hello"という文字列が"hello%"となり前方一致検索で利用できるようになります。

where name like /*name.asPrefix()*/

同様にasInfix()を呼び出すと中間一致検索用の文字列に変換し、asSuffix()を呼び出すと後方一致検索用の文字列に変換します。

escape()は特別な文字をエスケープします。例えば、"he%llo_"という文字列を"he\%llo\_"のような文字列に変換します。

最終更新 May 24, 2022: Fix typos (27dead1)