🦜

Kotlin製ORマッパーexposedはどのように動いているのか?

2023/09/09に公開

TL;DR

  • DSL にて DB へ問い合わせを行う際、Kotlin のコードで書かれたクエリをどのようにネイティブクエリに変換しているのか説明する
  • DB から取得した結果セットを Kotlin の型とどのようにマッピングしているのか説明する
  • OR マッパーの仕組みを説明する

この記事で解決する疑問

例えばDEPARTMENTというテーブルに対してデータを全件取得しようとすれば、以下のように DSL を記述することになります。

Department.kt
object Department : Table("DEPARTMENT") {
    val departmentId = integer("DEPARTMENT_ID") //部署ID
    val departmentName = varchar("DEPARTMENT_NAME", 32) //部署名
    val createdAt = datetime("CREATED_AT") //作成日時
    val updatedAt = datetime("UPDATED_AT") //更新日時
}
val results = Department.slice(Department.departmentId, Department.departmentName).selectAll().toList()

この時点で結果セットがresultsにセットされており、取得した DB の値にアクセスするには以下のように記述します。

val results = Department.slice(Department.departmentId, Department.departmentName).selectAll().toList()
results.map{
    println(it[Department.departmentId]) //部署ID
}

SQL そのものを書くことはなくデータを取得し、Kotlin の世界でそれを簡潔な記述で扱えています。

従来のprepareStatementexecuteQueryresultSetなどの手続きが一切簡略化されており素晴らしいです。

…が、なぜこのように書くと SQL が投げられて、うまくデータが取得できているのか不思議に思いませんか?

私は不思議でしょうがありませんでした。

そこで、exposedがいかに簡略化してくれているかをあえて覗いてみた結果を若干端折りながらも、順番に説明していこうというのが本記事の趣旨です。

Slice とは何か?

まずは問題を分解。

val slice = Department.slice(Department.departmentId, Department.departmentName)
val selectAll = slice.selectAll()
val results = selectAll.toList()

とし、sliceとは何か?を見てみたいと思います。

コードからDepartmentのメソッドであるらしいことは察せるので、まずは Department の中を見ます。

Department.kt
package example.koin.data.model

import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.javatime.datetime

object Department : Table("DEPARTMENT") {
    val departmentId = integer("DEPARTMENT_ID") //部署ID
    val departmentName = varchar("DEPARTMENT_NAME", 32) //部署名
    val createdAt = datetime("CREATED_AT") //作成日時
    val updatedAt = datetime("UPDATED_AT") //更新日時
}

Departmentにはsliceというメソッドがないので、スーパークラスであるTableの中にあるだろうと...と思いますがTableの中にもありません。

ので、さらに先祖を辿っていきます。

Table.kt
open class Table(name: String = "") : ColumnSet(), DdlAware {

TableにはさらにスーパークラスのColumnSetがいるようです。中を見ましょう。

abstract class ColumnSet : FieldSet {
    //略

    /** Specifies a subset of [columns] of this [ColumnSet]. */
    fun slice(column: Expression<*>, vararg columns: Expression<*>): FieldSet = Slice(this, listOf(column) + columns)

    /** Specifies a subset of [columns] of this [ColumnSet]. */
    fun slice(columns: List<Expression<*>>): FieldSet = Slice(this, columns)
}

ありました。sliceは列を受け取って、Sliceというインスタンスを返すようです。

ここで重要なのが、sliceメソッドの戻り値はSliceではなく、FieldSet型であるということです。

これは後で重要になってくるので、脳内メモリに貯めておいてください。

一応FieldSetについて見ておきましょう。FieldSetはインターフェースです。

FieldSet.kt
interface FieldSet {
    /** Return the column set that contains this field set. */
    val source: ColumnSet

    /** Returns the field of this field set. */
    val fields: List<Expression<*>>

    /**
     * Returns all real fields, unrolling composite [CompositeColumn] if present
     */
    val realFields: List<Expression<*>>
        get() {
            val unrolled = ArrayList<Expression<*>>(fields.size)

            fields.forEach {
                if (it is CompositeColumn<*>) {
                    unrolled.addAll(it.getRealColumns())
                } else unrolled.add(it)
            }

            return unrolled
        }
}

話をSliceに戻します。

SliceFieldSetの具象クラスです。ただし、sourcefieldsの二つのメンバ変数のみ持つだけのようです。

Slice.kt
class Slice(override val source: ColumnSet, override val fields: List<Expression<*>>) : FieldSet

では、今回作成されたSliceインスタンスが何を実の値として持っているのかだけ確認しておきましょう。

val slice = Department.slice(Department.departmentId, Department.departmentName)
fun slice(column: Expression<*>, vararg columns: Expression<*>): FieldSet = Slice(this, listOf(column) + columns)

より、source=Department, fields=[Department.departmentId, Department.departmentName]です。

selectAll()とはなにか?

ではコマを一つ進めます。

val slice = Department.slice(Department.departmentId, Department.departmentName)
val selectAll = slice.selectAll()
val results = selectAll.toList()

次はselectAll()について見ていきたいと思います。

ここでみなさんの勘違いを一つ溶かしておきたいのですが、exposed は selectAll()や select() のタイミングでクエリを発行しているわけではありません

「じゃあ、いつ?」とお思いかと察しますが、まずは焦らずselectAll()がクエリを発行していないことを確認していきましょう。

selectAll()はどこに定義されているのか?

先ほど確認しましたが、Sliceはメソッドを持っていません。

そこでFieldSetを見ると、そもそもインターフェースFieldSetselectAll()自体定義されていないのです。

「は???ではどこにいるんだ?」という話なのですが、結論から言うとQueries.ktというファイルにFieldSetの拡張関数として定義されています。

Queries.kt
fun FieldSet.selectAll(): Query = Query(this, null)

見るとselectAll()はあくまでQueryというインスタンスを返しているに過ぎないようです。

ではQueryの中を見てみましょう。

Query.kt
open class Query(override var set: FieldSet, where: Op<Boolean>?) : AbstractQuery<Query>(set.source.targetTables()) {
    var distinct: Boolean = false
        protected set

    var groupedByColumns: List<Expression<*>> = mutableListOf()
        private set

    var having: Op<Boolean>? = null
        private set

    private var forUpdate: ForUpdateOption? = null

    // private set
    var where: Op<Boolean>? = where
        private set

    override val queryToExecute: Statement<ResultSet> get() {
        val distinctExpressions = set.fields.distinct()
        return if (distinctExpressions.size < set.fields.size) {
            copy().adjustSlice { slice(distinctExpressions) }
        } else {
            this
        }
    }

    override fun copy(): Query = Query(set, where).also { copy ->
        copyTo(copy)
        copy.distinct = distinct
        copy.groupedByColumns = groupedByColumns.toMutableList()
        copy.having = having
        copy.forUpdate = forUpdate
    }

    override fun forUpdate(option: ForUpdateOption): Query {
        this.forUpdate = option
        return this
    }

    override fun notForUpdate(): Query {
        forUpdate = ForUpdateOption.NoForUpdateOption
        return this
    }

    override fun withDistinct(value: Boolean): Query = apply {
        distinct = value
    }

    /**
     * Changes [set.fields] field of a Query, [set.source] will be preserved
     * @param body builder for new column set, current [set.source] used as a receiver and current [set] as an argument, you are expected to slice it
     * @sample org.jetbrains.exposed.sql.tests.shared.dml.AdjustQueryTests.testAdjustQuerySlice
     */
    fun adjustSlice(body: ColumnSet.(FieldSet) -> FieldSet): Query = apply { set = set.source.body(set) }

    /**
     * Changes [set.source] field of a Query, [set.fields] will be preserved
     * @param body builder for new column set, previous value used as a receiver
     * @sample org.jetbrains.exposed.sql.tests.shared.dml.AdjustQueryTests.testAdjustQueryColumnSet
     */
    fun adjustColumnSet(body: ColumnSet.() -> ColumnSet): Query {
        return adjustSlice { oldSlice -> body().slice(oldSlice.fields) }
    }

    /**
     * Changes [where] field of a Query.
     * @param body new WHERE condition builder, previous value used as a receiver
     * @sample org.jetbrains.exposed.sql.tests.shared.dml.AdjustQueryTests.testAdjustQueryWhere
     */
    fun adjustWhere(body: Op<Boolean>?.() -> Op<Boolean>): Query = apply { where = where.body() }

    /**
     * Changes [having] field of a Query.
     * @param body new HAVING condition builder, previous value used as a receiver
     * @sample org.jetbrains.exposed.sql.tests.shared.dml.AdjustQueryTests.testAdjustQueryHaving
     */
    fun adjustHaving(body: Op<Boolean>?.() -> Op<Boolean>): Query = apply { having = having.body() }

    fun hasCustomForUpdateState() = forUpdate != null
    fun isForUpdate() = (forUpdate?.let { it != ForUpdateOption.NoForUpdateOption } ?: false) && currentDialect.supportsSelectForUpdate()

    override fun PreparedStatementApi.executeInternal(transaction: Transaction): ResultSet? {
        val fetchSize = this@Query.fetchSize ?: transaction.db.defaultFetchSize
        if (fetchSize != null) {
            this.fetchSize = fetchSize
        }
        return executeQuery()
    }

    override fun prepareSQL(builder: QueryBuilder): String {
        builder {
            append("SELECT ")

            if (count) {
                append("COUNT(*)")
            } else {
                if (distinct) {
                    append("DISTINCT ")
                }
                set.realFields.appendTo { +it }
            }
            if (set.source != Table.Dual || currentDialect.supportsDualTableConcept) {
                append(" FROM ")
                set.source.describe(transaction, this)
            }

            where?.let {
                append(" WHERE ")
                +it
            }

            if (!count) {
                if (groupedByColumns.isNotEmpty()) {
                    append(" GROUP BY ")
                    groupedByColumns.appendTo {
                        +((it as? ExpressionAlias)?.aliasOnlyExpression() ?: it)
                    }
                }

                having?.let {
                    append(" HAVING ")
                    append(it)
                }

                if (orderByExpressions.isNotEmpty()) {
                    append(" ORDER BY ")
                    orderByExpressions.appendTo { (expression, sortOrder) ->
                        currentDialect.dataTypeProvider.precessOrderByClause(this, expression, sortOrder)
                    }
                }

                limit?.let {
                    append(" ")
                    append(currentDialect.functionProvider.queryLimit(it, offset, orderByExpressions.isNotEmpty()))
                }
            }

            if (isForUpdate()) {
                forUpdate?.apply {
                    append(" $querySuffix")
                }
            }
        }
        return builder.toString()
    }

    fun groupBy(vararg columns: Expression<*>): Query {
        for (column in columns) {
            (groupedByColumns as MutableList).add(column)
        }
        return this
    }

    fun having(op: SqlExpressionBuilder.() -> Op<Boolean>): Query {
        val oop = SqlExpressionBuilder.op()
        if (having != null) {
            error("HAVING clause is specified twice. Old value = '$having', new value = '$oop'")
        }
        having = oop
        return this
    }

    override fun count(): Long {
        return if (distinct || groupedByColumns.isNotEmpty() || limit != null) {
            fun Column<*>.makeAlias() =
                alias(transaction.db.identifierManager.quoteIfNecessary("${table.tableName}_$name"))

            val originalSet = set
            try {
                var expInx = 0
                adjustSlice {
                    slice(
                        originalSet.fields.map {
                            it as? ExpressionAlias<*> ?: ((it as? Column<*>)?.makeAlias() ?: it.alias("exp${expInx++}"))
                        }
                    )
                }

                alias("subquery").selectAll().count()
            } finally {
                set = originalSet
            }
        } else {
            try {
                count = true
                transaction.exec(this) { rs ->
                    rs.next()
                    rs.getLong(1).also { rs.close() }
                }!!
            } finally {
                count = false
            }
        }
    }

    override fun empty(): Boolean {
        val oldLimit = limit
        try {
            if (!isForUpdate()) limit = 1
            val resultSet = transaction.exec(this)!!
            return !resultSet.next().also { resultSet.close() }
        } finally {
            limit = oldLimit
        }
    }
}

さて、prepareSQL()のような「おや?👀」というメソッドが登場していますね。ですが焦らないでください。このメソッドの出番はまだ先です。

ここでQueryクラスの全体を貼ったのは、コンストラクタや初期化ブロックがいないよね?ということを確認するためです。

つまり、Queryクラスそれ自体ではなんらかのクエリの呼び出しをしているわけではないということです。

では、スーパークラスでも同じことを確認しておきましょう。

AbstractQuery.kt
abstract class AbstractQuery<T : AbstractQuery<T>>(targets: List<Table>) : SizedIterable<ResultRow>, Statement<ResultSet>(StatementType.SELECT, targets) {
    protected val transaction get() = TransactionManager.current()

    var orderByExpressions: List<Pair<Expression<*>, SortOrder>> = mutableListOf()
        private set

    var limit: Int? = null
        protected set
    var offset: Long = 0
        private set
    var fetchSize: Int? = null
        private set

    abstract val set: FieldSet

    protected fun copyTo(other: AbstractQuery<T>) {
        other.orderByExpressions = orderByExpressions.toMutableList()
        other.limit = limit
        other.offset = offset
        other.fetchSize = fetchSize
    }

    override fun prepareSQL(transaction: Transaction) = prepareSQL(QueryBuilder(true))

    abstract fun prepareSQL(builder: QueryBuilder): String

    override fun arguments() = QueryBuilder(true).let {
        prepareSQL(it)
        if (it.args.isNotEmpty()) listOf(it.args) else emptyList()
    }

    abstract fun withDistinct(value: Boolean = true): T

    override fun limit(n: Int, offset: Long): T = apply {
        limit = n
        this.offset = offset
    } as T

    fun orderBy(column: Expression<*>, order: SortOrder = SortOrder.ASC): T = orderBy(column to order)

    override fun orderBy(vararg order: Pair<Expression<*>, SortOrder>): T = apply {
        (orderByExpressions as MutableList).addAll(order)
    } as T

    fun fetchSize(n: Int): T = apply {
        fetchSize = n
    } as T

    protected var count: Boolean = false

    protected abstract val queryToExecute: Statement<ResultSet>

    override fun iterator(): Iterator<ResultRow> {
        val resultIterator = ResultIterator(transaction.exec(queryToExecute)!!)
        return if (transaction.db.supportsMultipleResultSets) {
            resultIterator
        } else {
            Iterable { resultIterator }.toList().iterator()
        }
    }

    private inner class ResultIterator(val rs: ResultSet) : Iterator<ResultRow> {
        private var hasNext = false
            set(value) {
                field = value
                if (!field) {
                    rs.statement?.close()
                    transaction.openResultSetsCount--
                }
            }

        private val fieldsIndex = set.realFields.toSet().mapIndexed { index, expression -> expression to index }.toMap()

        init {
            hasNext = rs.next()
            if (hasNext) trackResultSet(transaction)
        }

        override operator fun next(): ResultRow {
            if (!hasNext) throw NoSuchElementException()
            val result = ResultRow.create(rs, fieldsIndex)
            hasNext = rs.next()
            return result
        }

        override fun hasNext(): Boolean = hasNext
    }

    companion object {
        private fun trackResultSet(transaction: Transaction) {
            val threshold = transaction.db.config.logTooMuchResultSetsThreshold
            if (threshold > 0 && threshold < transaction.openResultSetsCount) {
                val message =
                    "Current opened result sets size ${transaction.openResultSetsCount} exceeds $threshold threshold for transaction ${transaction.id} "
                val stackTrace = Exception(message).stackTraceToString()
                exposedLogger.error(stackTrace)
            }
            transaction.openResultSetsCount++
        }
    }
}

ここでAbstractQueryクラスでも、コンストラクタや初期化ブロックがありません。

同じようにSizedIterableStatementを見ておきます。

SizedIterable.kt
interface SizedIterable<out T> : Iterable<T> {
    fun limit(n: Int, offset: Long = 0): SizedIterable<T>
    fun count(): Long
    fun empty(): Boolean
    fun forUpdate(option: ForUpdateOption = ForUpdateOption.ForUpdate): SizedIterable<T> = this
    fun notForUpdate(): SizedIterable<T> = this
    fun copy(): SizedIterable<T>
    fun orderBy(vararg order: Pair<Expression<*>, SortOrder>): SizedIterable<T>
}

SizedIterableはインターフェースで、さらにIterableも実装されています。

そのIterableの具象メソッドiterator()AbstractQueryにて実装されています。

このことはあとで重要になってくるので、脳内メモリに刻んでおいてください。

Statement.kt
abstract class Statement<out T>(val type: StatementType, val targets: List<Table>) {

    abstract fun PreparedStatementApi.executeInternal(transaction: Transaction): T?

    abstract fun prepareSQL(transaction: Transaction): String

    abstract fun arguments(): Iterable<Iterable<Pair<IColumnType, Any?>>>

    open fun prepared(transaction: Transaction, sql: String): PreparedStatementApi =
        transaction.connection.prepareStatement(sql, false)

    open val isAlwaysBatch: Boolean = false

    fun execute(transaction: Transaction): T? = transaction.exec(this)

    internal fun executeIn(transaction: Transaction): Pair<T?, List<StatementContext>> {
        val arguments = arguments()
        val contexts = if (arguments.any()) {
            arguments.map { args ->
                val context = StatementContext(this, args)
                Transaction.globalInterceptors.forEach { it.beforeExecution(transaction, context) }
                transaction.interceptors.forEach { it.beforeExecution(transaction, context) }
                context
            }
        } else {
            val context = StatementContext(this, emptyList())
            Transaction.globalInterceptors.forEach { it.beforeExecution(transaction, context) }
            transaction.interceptors.forEach { it.beforeExecution(transaction, context) }
            listOf(context)
        }

        val statement = try {
            prepared(transaction, prepareSQL(transaction))
        } catch (e: SQLException) {
            throw ExposedSQLException(e, contexts, transaction)
        }
        contexts.forEachIndexed { i, context ->
            statement.fillParameters(context.args)
            // REVIEW
            if (contexts.size > 1 || isAlwaysBatch) statement.addBatch()
        }
        if (!transaction.db.supportsMultipleResultSets) {
            transaction.closeExecutedStatements()
        }

        transaction.currentStatement = statement
        val result = try {
            statement.executeInternal(transaction)
        } catch (e: SQLException) {
            throw ExposedSQLException(e, contexts, transaction)
        }
        transaction.currentStatement = null
        transaction.executedStatements.add(statement)

        Transaction.globalInterceptors.forEach { it.afterExecution(transaction, contexts, statement) }
        transaction.interceptors.forEach { it.afterExecution(transaction, contexts, statement) }
        return result to contexts
    }
}

みなさんの脳内メモリをお助けすると、今回の場合StatementtypeにはSELECTが、targets[Department]になっています。

さて、ここまで見てきましたがクエリが実行されている余地があったでしょうか?

…ありませんでしたよね?

あくまで諸々の値がセットされたQueryインスタンスが返ってきただけです。

ではいつ実行されるのか?というのを次の章で見ていきます。

Kotlin における iterator パターン

では最後のコマに進みましょう。

ここからいよいよ本記事のアジェンダの核心に迫っていきます。

ここまで来ればもうお分かりかと思うのですが、クエリの発行と結果の取得は selectAll を toList()したタイミングでおこなっています。

val slice = Department.slice(Department.departmentId, Department.departmentName)
val selectAll = slice.selectAll()
val results = selectAll.toList()

では、なぜそんなことが可能なのか?

それは Kotlin のコレクション API 設計のおかげです。

Kotlin で toList()関数を呼び出すと、リストを生成するためにiterator()が呼ばれます。

map関数なども同じで、元のコレクションの要素に関数を適用して新しいコレクションを生成するにあたり、元のコレクション要素のアクセスするためにiterator()が呼ばれるのです。

そう、この時に使われるiterator()こそがAbstractQueryクラスのiterator()なのです。

では改めて中をよく見てみると…

AbstractQuery.kt
override fun iterator(): Iterator<ResultRow> {
    val resultIterator = ResultIterator(transaction.exec(queryToExecute)!!)
    return if (transaction.db.supportsMultipleResultSets) {
        resultIterator
    } else {
        Iterable { resultIterator }.toList().iterator()
    }
}

なにやらクエリを投げてそうなtransaction.exec(queryToExecute)というものが見つかりますね。

ここからexec()の中を見ていくことで、どのようにクエリが実行されているのか?を知ることができます。

ここでも皆さんの脳内メモリを補足しておくと、queryToExecuteQueryクラスでセットされています。

実態は、Statementです。

このことを頭に置きながら、Transactionexecメソッドを見ていきます。

クエリはどのように発行されるのか?

長々と書かれていますが、クエリを発行している実態はstmt.executeIn(this)です。

Transaction.kt
    fun <T, R> exec(stmt: Statement<T>, body: Statement<T>.(T) -> R): R? {
        statementCount++

        val start = System.nanoTime()
        val answer = stmt.executeIn(this)
        val delta = (System.nanoTime() - start).let { TimeUnit.NANOSECONDS.toMillis(it) }

        val lazySQL = lazy(LazyThreadSafetyMode.NONE) {
            answer.second.map { it.sql(this) }.distinct().joinToString()
        }

        duration += delta

        if (debug) {
            statements.append(describeStatement(delta, lazySQL.value))
            statementStats.getOrPut(lazySQL.value, { 0 to 0L }).let { (count, time) ->
                statementStats[lazySQL.value] = (count + 1) to (time + delta)
            }
        }

        if (delta > (warnLongQueriesDuration ?: Long.MAX_VALUE)) {
            exposedLogger.warn("Long query: ${describeStatement(delta, lazySQL.value)}", LongQueryException())
        }

        return answer.first?.let { stmt.body(it) }
    }

つまり、StatementクラスのexecuteInメソッドを見る必要があります。

executeInメソッドの全体像は以下の通りなのですが、今回は要点を絞って確認します。

Statement.kt
    internal fun executeIn(transaction: Transaction): Pair<T?, List<StatementContext>> {
        val arguments = arguments()
        val contexts = if (arguments.any()) {
            arguments.map { args ->
                val context = StatementContext(this, args)
                Transaction.globalInterceptors.forEach { it.beforeExecution(transaction, context) }
                transaction.interceptors.forEach { it.beforeExecution(transaction, context) }
                context
            }
        } else {
            val context = StatementContext(this, emptyList())
            Transaction.globalInterceptors.forEach { it.beforeExecution(transaction, context) }
            transaction.interceptors.forEach { it.beforeExecution(transaction, context) }
            listOf(context)
        }

        val statement = try {
            prepared(transaction, prepareSQL(transaction))
        } catch (e: SQLException) {
            throw ExposedSQLException(e, contexts, transaction)
        }
        contexts.forEachIndexed { i, context ->
            statement.fillParameters(context.args)
            // REVIEW
            if (contexts.size > 1 || isAlwaysBatch) statement.addBatch()
        }
        if (!transaction.db.supportsMultipleResultSets) {
            transaction.closeExecutedStatements()
        }

        transaction.currentStatement = statement
        val result = try {
            statement.executeInternal(transaction)
        } catch (e: SQLException) {
            throw ExposedSQLException(e, contexts, transaction)
        }
        transaction.currentStatement = null
        transaction.executedStatements.add(statement)

        Transaction.globalInterceptors.forEach { it.afterExecution(transaction, contexts, statement) }
        transaction.interceptors.forEach { it.afterExecution(transaction, contexts, statement) }
        return result to contexts
    }

要点とは、

  • クエリを組み立てている処理 = prepared(transaction, prepareSQL(transaction))
  • クエリを発行する処理 = statement.executeInternal(transaction)

この 2 点です。

クエリはどのように組み立てられるのか?

prepared(transaction, prepareSQL(transaction))prepareSQLがクエリを実際に組み立てている処理です。

これがどこにあるかというと、最初にちらっと話したQueryクラスです。

Query
    override fun prepareSQL(builder: QueryBuilder): String {
        builder {
            append("SELECT ")

            if (count) {
                append("COUNT(*)")
            } else {
                if (distinct) {
                    append("DISTINCT ")
                }
                set.realFields.appendTo { +it }
            }
            if (set.source != Table.Dual || currentDialect.supportsDualTableConcept) {
                append(" FROM ")
                set.source.describe(transaction, this)
            }

            where?.let {
                append(" WHERE ")
                +it
            }

            if (!count) {
                if (groupedByColumns.isNotEmpty()) {
                    append(" GROUP BY ")
                    groupedByColumns.appendTo {
                        +((it as? ExpressionAlias)?.aliasOnlyExpression() ?: it)
                    }
                }

                having?.let {
                    append(" HAVING ")
                    append(it)
                }

                if (orderByExpressions.isNotEmpty()) {
                    append(" ORDER BY ")
                    orderByExpressions.appendTo { (expression, sortOrder) ->
                        currentDialect.dataTypeProvider.precessOrderByClause(this, expression, sortOrder)
                    }
                }

                limit?.let {
                    append(" ")
                    append(currentDialect.functionProvider.queryLimit(it, offset, orderByExpressions.isNotEmpty()))
                }
            }

            if (isForUpdate()) {
                forUpdate?.apply {
                    append(" $querySuffix")
                }
            }
        }
        return builder.toString()
    }

SQL を組み立てている感がものすごくありますね!

ここでSELECT DEPARTMENT.DEPARTMENT_ID, DEPARTMENT.DEPARTMENT_NAME FROM DEPARTMENTというクエリができあがります。

whereがある場合や、having, order byなども確認できます。

一応補足として、set.source.describe(transaction, this)において、set.source = Department, describeTableクラスの以下のメソッドです。

Table.kt
override fun describe(s: Transaction, queryBuilder: QueryBuilder): Unit = queryBuilder { append(s.identity(this@Table)) }

set.source.describe(transaction, this)の結果としては、queryBuilderDEPARTMENTが append されることになります。

クエリはどのように発行されているのか?

クエリ発行はstatement.executeInternal(transaction)により行われていますが、この実態はQueryクラスのexecuteInternalメソッドです。

Query.kt
    override fun PreparedStatementApi.executeInternal(transaction: Transaction): ResultSet? {
        val fetchSize = this@Query.fetchSize ?: transaction.db.defaultFetchSize
        if (fetchSize != null) {
            this.fetchSize = fetchSize
        }
        return executeQuery()
    }

ここで実行されるexecuteQueryの実態は、JdbcPreparedStatementImplexecuteQueryメソッドです。

JdbcPreparedStatementImpl.kt
    override fun executeQuery(): ResultSet = statement.executeQuery()

ここで先ほど組み立てられたクエリが発行され、結果セットを取得しているわけです。

結果セットはどのようにマッピングされているのか?

さて、クエリを組み立て、発行するまでがどうなっているかわかりました。

あと一息、結果セットがどのようにマッピングされているのか?が理解できれば、単一テーブルに対しての OR マッパーの仕組みを見切れます。

ところで、クエリ発行はAbstractQueryクラスに定義されたiteratorメソッドで行われていました。

AbstractQuery.kt
    override fun iterator(): Iterator<ResultRow> {
        val resultIterator = ResultIterator(transaction.exec(queryToExecute)!!)
        return if (transaction.db.supportsMultipleResultSets) {
            resultIterator
        } else {
            Iterable { resultIterator }.toList().iterator()
        }
    }

ここでResultIteratorというものを見ていきたいと思います。ResultIteratorAbstractQueryの内部クラスです。

ResultIterator
    private inner class ResultIterator(val rs: ResultSet) : Iterator<ResultRow> {
        private var hasNext = false
            set(value) {
                field = value
                if (!field) {
                    rs.statement?.close()
                    transaction.openResultSetsCount--
                }
            }

        private val fieldsIndex = set.realFields.toSet().mapIndexed { index, expression -> expression to index }.toMap()

        init {
            hasNext = rs.next()
            if (hasNext) trackResultSet(transaction)
        }

        override operator fun next(): ResultRow {
            if (!hasNext) throw NoSuchElementException()
            val result = ResultRow.create(rs, fieldsIndex)
            hasNext = rs.next()
            return result
        }

        override fun hasNext(): Boolean = hasNext
    }

さて、、、iterator パターンをご存知の方であればみた時点で察せそうですが…

ここがiteratorによるループ処理の本体になってますね。

まず、結果セットをコンストラクタとして受け取り、次にinitブロックの中が実行されます。

するとnext()によりResultSetの最後に到達するまで、順にnext()メソッドが実行されていきます。

このnext()内部のval result = ResultRow.create(rs, fieldsIndex)がマッピングの本体です。

ではResultRowcreateメソッドについて見てみます。

ResultRow.kt
class ResultRow(
    val fieldIndex: Map<Expression<*>, Int>,
    private val data: Array<Any?> = arrayOfNulls<Any?>(fieldIndex.size)
) {
    //略
    companion object {
        fun create(rs: ResultSet, fieldsIndex: Map<Expression<*>, Int>): ResultRow {
            return ResultRow(fieldsIndex).apply {
                fieldsIndex.forEach { (field, index) ->
                    val columnType = (field as? Column<*>)?.columnType
                    val value = if (columnType != null) columnType.readObject(rs, index + 1) else rs.getObject(index + 1)
                    data[index] = value
                }
            }
        }
        //略
    }
}

ResultRowfieldsIndexdataをメンバ変数として持ちます。

fieldsIndexResultIteratorから受け取っており、この中に格納されているのはSliceが持つTableの列情報です。

これらを使いながらdataSliceで指定した列順に値を Array<Any?>型で保持しています。

そう、あくまでResultRowの内部では、値はインデックスで管理されているのです。

では、なぜ以下のように値へのアクセスが可能なのでしょうか?

val slice = Department.slice(Department.departmentId, Department.departmentName)
val selectAll = slice.selectAll().toList()
println(selectAll[0][Department.departmentId])

それはResultRowのカスタムゲッターを見ることで理解できます。

ResultRow.kt
    operator fun <T> get(c: Expression<T>): T {
        if (c in lookUpCache) return lookUpCache[c] as T

        val d = getRaw(c)

        if (d == null && c is Column<*> && c.dbDefaultValue != null && !c.columnType.nullable) {
            exposedLogger.warn(
                "Column ${TransactionManager.current().fullIdentity(c)} is marked as not null, " +
                    "has default db value, but returns null. Possible have to re-read it from DB."
            )
        }

        val result = database?.dialect?.let {
            withDialect(it) {
                rawToColumnValue(d, c)
            }
        } ?: rawToColumnValue(d, c)
        lookUpCache[c] = result
        return result
    }

    @Suppress("UNCHECKED_CAST")
    private fun <T> getRaw(c: Expression<T>): T? {
        if (c is CompositeColumn<T>) {
            val rawParts = c.getRealColumns().associateWith { getRaw(it) }
            return c.restoreValueFromParts(rawParts)
        }

        val index = fieldIndex[c]
            ?: ((c as? Column<*>)?.columnType as? EntityIDColumnType<*>)?.let { fieldIndex[it.idColumn] }
            ?: fieldIndex.keys.firstOrNull { exp ->
                when (exp) {
                    // exp is Column<*> && exp.table is Alias<*> -> exp.table.delegate == c
                    is Column<*> -> (exp.columnType as? EntityIDColumnType<*>)?.idColumn == c
                    is ExpressionAlias<*> -> exp.delegate == c
                    else -> false
                }
            }?.let { fieldIndex[it] }
            ?: error("$c is not in record set")

        return data[index] as T?
    }

    @Suppress("UNCHECKED_CAST")
    private fun <T> rawToColumnValue(raw: T?, c: Expression<T>): T {
        return when {
            raw == null -> null
            raw == NotInitializedValue -> error("$c is not initialized yet")
            c is ExpressionAlias<T> && c.delegate is ExpressionWithColumnType<T> -> c.delegate.columnType.valueFromDB(raw)
            c is ExpressionWithColumnType<T> -> c.columnType.valueFromDB(raw)
            c is Op.OpBoolean -> BooleanColumnType.INSTANCE.valueFromDB(raw)
            else -> raw
        } as T
    }

重要なのはgetRaw()rawToColumnValue()ですね。

まずgetRaw()で指定されたプロパティに紐づくインデックスを取得し、dataから値を取り出しています。

今回のサンプルであるDepartment.departmentIdを指定した場合には0が取得できることになります。

あとのrawToColumnValueですが、こちらはDepartment.departmentIdのように通常の列を指定した場合ではなく、列別名などを指定した場合の値と型の取得を担っているようです。

以上が結果セットがどのようにマッピングされているかの実態です。

おわりに

やっぱり OSS のライブラリやフレームワークの裏側のコードを読んでいくは楽しく、新たな発見が多いです。

継承や多態性の使い方、それからiterator()の使い方に面白いなと感じました。

今回は一番シンプルな単一テーブルに対する問い合わせについて見ましたが、サブクエリや JOIN 、登録更新削除がどのように解釈されていっているのかもまた時間があるときに見切りたいですね。

メンバー募集中!

サーバーサイド Kotlin コミュニティを作りました、ぜひご参加ください!!

https://serverside-kt.connpass.com/

また関西在住のソフトウェア開発者を中心に、関西エンジニアコミュニティを一緒に盛り上げてくださる方を募集しています。

よろしければ Conpass からメンバー登録よろしくお願いいたします。

https://blessingsoftware.connpass.com/

Discussion