KotlinでのDSLの構築

1. 概要

このチュートリアルでは、*強力なKotlin言語機能を使用して、タイプセーフなDSLを構築する方法を確認します。*
この例では、概念を説明するのに十分な大きさの、SQLクエリを作成するためのシンプルなツールを作成します。
*一般的な考え方は、呼び出し時にクエリビルダーの状態を変更する静的に型指定されたユーザー提供の関数リテラルを使用することです。*それらのすべてが呼び出された後、ビルダーの状態が検証され、結果のSQL文字列が生成されます。

2. エントリポイントの定義

機能のエントリポイントを定義することから始めましょう。
class SqlSelectBuilder {
    fun build(): String {
        TODO("implement")
    }
}

fun query(initializer: SqlSelectBuilder.() -> Unit): SqlSelectBuilder {
    return SqlSelectBuilder().apply(initializer)
}
次に、定義された関数を単純に使用できます。
val sql = query {
}.build()

3. 列の追加

使用するターゲット列の定義のサポートを追加しましょう。 DSLでの表示は次のとおりです。
query {
    select("column1", "column2")
}
そして、_select_関数の実装:
class SqlSelectBuilder {

    private val columns = mutableListOf<String>()

    fun select(vararg columns: String) {
        if (columns.isEmpty()) {
            throw IllegalArgumentException("At least one column should be defined")
        }
        if (this.columns.isNotEmpty()) {
            throw IllegalStateException("Detected an attempt to re-define columns to fetch. "
              + "Current columns list: "
              + "${this.columns}, new columns list: $columns")
        }
        this.columns.addAll(columns)
    }
}

4. テーブルの定義

また、使用するターゲットテーブルの指定を許可する必要があります。
query {
    select ("column1", "column2")
    from ("myTable")
}
_from_関数は、クラスプロパティで受け取ったテーブル名を設定するだけです。
class SqlSelectBuilder {

    private lateinit var table: String

    fun from(table: String) {
        this.table = table
    }
}

5. 最初のマイルストーン

実際、単純なクエリを作成してテストするのに十分な数になりました。 やってみましょう!
*最初に、_SqlSelectBuilder.build_メソッドを実装する必要があります:*
class SqlSelectBuilder {

    fun build(): String {
        if (!::table.isInitialized) {
            throw IllegalStateException("Failed to build an sql select - target table is undefined")
        }
        return toString()
    }

    override fun toString(): String {
        val columnsToFetch =
                if (columns.isEmpty()) {
                    "*"
                } else {
                    columns.joinToString(", ")
                }
        return "select $columnsToFetch from $table"
    }
}
これで、いくつかのテストを導入できます。
private fun doTest(expected: String, sql: SqlSelectBuilder.() -> Unit) {
    assertThat(query(sql).build()).isEqualTo(expected)
}

@Test
fun `when no columns are specified then star is used`() {
    doTest("select * from table1") {
        from ("table1")
    }
}
@Test
fun `when no condition is specified then correct query is built`() {
    doTest("select column1, column2 from table1") {
        select("column1", "column2")
        from ("table1")
    }
}

6. _AND_条件

ほとんどの場合、クエリで条件を指定する必要があります。
DSLの外観を定義することから始めましょう。
query {
    from("myTable")
    where {
        "column3" eq 4
        "column3" eq null
    }
}
これらの条件は、実際にはSQL ANDオペランドなので、同じ概念をソースコードに導入しましょう。
class SqlSelectBuilder {
    fun where(initializer: Condition.() -> Unit) {
        condition = And().apply(initializer)
    }
}

abstract class Condition

class And : Condition()

class Eq : Condition()
クラスを1つずつ実装しましょう。
abstract class Condition {
    infix fun String.eq(value: Any?) {
        addCondition(Eq(this, value))
    }
}
class Eq(private val column: String, private val value: Any?) : Condition() {

    init {
        if (value != null && value !is Number && value !is String) {
            throw IllegalArgumentException(
              "Only <null>, numbers and strings values can be used in the 'where' clause")
        }
    }

    override fun addCondition(condition: Condition) {
        throw IllegalStateException("Can't add a nested condition to the sql 'eq'")
    }

    override fun toString(): String {
        return when (value) {
            null -> "$column is null"
            is String -> "$column = '$value'"
            else -> "$column = $value"
        }
    }
}
最後に、条件のリストを保持し、_addCondition_メソッドを実装する_And_クラスを作成します。
class And : Condition() {

    private val conditions = mutableListOf<Condition>()

    override fun addCondition(condition: Condition) {
        conditions += condition
    }

    override fun toString(): String {
        return if (conditions.size == 1) {
            conditions.first().toString()
        } else {
            conditions.joinToString(prefix = "(", postfix = ")", separator = " and ")
        }
    }
}
ここで難しいのは、DSL基準をサポートすることです。 そのために、d_Condition.eq_ infix String拡張関数として宣言します。 そのため、従来どおり_column.eq(value)_のように使用することも、ドットと括弧なしで使用することもできます(__column eq valu__e)。
関数は_Condition_クラスのコンテキストで定義されているため、使用できます(_SqlSelectBuilder.where_は_Condition_のコンテキストで実行される関数リテラルを受け取ることに注意してください)。
これで、すべてが期待どおりに機能することを確認できます。
@Test
fun `when a list of conditions is specified then it's respected`() {
    doTest("select * from table1 where (column3 = 4 and column4 is null)") {
        from ("table1")
        where {
            "column3" eq 4
            "column4" eq null
        }
    }
}

7. _OR_条件

演習の最後の部分は、SQL OR条件のサポートです。 いつものように、最初にDSLでどのように見えるかを定義しましょう。
query {
    from("myTable")
    where {
        "column1" eq 4
        or {
            "column2" eq null
            "column3" eq 42
        }
    }
}
次に、実装を提供します。 ORとANDは非常に似ているため、既存の実装を再利用できます。
open class CompositeCondition(private val sqlOperator: String) : Condition() {
    private val conditions = mutableListOf<Condition>()

    override fun addCondition(condition: Condition) {
        conditions += condition
    }

    override fun toString(): String {
        return if (conditions.size == 1) {
            conditions.first().toString()
        } else {
            conditions.joinToString(prefix = "(", postfix = ")", separator = " $sqlOperator ")
        }
    }
}

class And : CompositeCondition("and")

class Or : CompositeCondition("or")
最後に、対応するサポートを条件サブDSLに追加します。
abstract class Condition {
    fun and(initializer: Condition.() -> Unit) {
        addCondition(And().apply(initializer))
    }

    fun or(initializer: Condition.() -> Unit) {
        addCondition(Or().apply(initializer))
    }
}
すべてが機能することを確認しましょう:
@Test
fun `when 'or' conditions are specified then they are respected`() {
    doTest("select * from table1 where (column3 = 4 or column4 is null)") {
        from ("table1")
        where {
            or {
                "column3" eq 4
                "column4" eq null
            }
        }
    }
}

@Test
fun `when either 'and' or 'or' conditions are specified then they are respected`() {
    doTest("select * from table1 where ((column3 = 4 or column4 is null) and column5 = 42)") {
        from ("table1")
        where {
            or {
                "column3" eq 4
                "column4" eq null
            }
            "column5" eq 42
        }
    }
}

8. 余分な楽しみ

このチュートリアルの範囲外ではありますが、DSLの拡張にも同じ概念を使用できます。 たとえば、LIKE、GROUP BY、HAVING、ORDER BYのサポートを追加することで機能を強化できます。 コメントに解決策を投稿してください!

9. 結論

この記事では、SQLクエリ用の簡単なDSLを構築する例を見てきました。 網羅的なガイドではありませんが、優れた基盤を確立し、KotlinのタイプセーフDSLアプローチ全体の概要を提供します。
通常通り、この記事の完全なソースコードはhttps://github.com/eugenp/tutorials/tree/master/core-kotlin/src/main/kotlin/com/baeldung/kotlin/dsl[over on GitHub]で入手できます。 。