Kotlin製ORマッパーexposedはどのように動いているのか?
TL;DR
- DSL にて DB へ問い合わせを行う際、Kotlin のコードで書かれたクエリをどのようにネイティブクエリに変換しているのか説明する
- DB から取得した結果セットを Kotlin の型とどのようにマッピングしているのか説明する
- OR マッパーの仕組みを説明する
この記事で解決する疑問
例えばDEPARTMENT
というテーブルに対してデータを全件取得しようとすれば、以下のように DSL を記述することになります。
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 の世界でそれを簡潔な記述で扱えています。
従来のprepareStatement
、executeQuery
、resultSet
などの手続きが一切簡略化されており素晴らしいです。
…が、なぜこのように書くと SQL が投げられて、うまくデータが取得できているのか不思議に思いませんか?
私は不思議でしょうがありませんでした。
そこで、exposed
がいかに簡略化してくれているかをあえて覗いてみた結果を若干端折りながらも、順番に説明していこうというのが本記事の趣旨です。
Slice とは何か?
まずは問題を分解。
val slice = Department.slice(Department.departmentId, Department.departmentName)
val selectAll = slice.selectAll()
val results = selectAll.toList()
とし、slice
とは何か?を見てみたいと思います。
コードからDepartment
のメソッドであるらしいことは察せるので、まずは Department の中を見ます。
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
の中にもありません。
ので、さらに先祖を辿っていきます。
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
はインターフェースです。
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
に戻します。
Slice
はFieldSet
の具象クラスです。ただし、source
とfields
の二つのメンバ変数のみ持つだけのようです。
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
を見ると、そもそもインターフェースFieldSet
はselectAll()
自体定義されていないのです。
「は???ではどこにいるんだ?」という話なのですが、結論から言うとQueries.kt
というファイルにFieldSet
の拡張関数として定義されています。
fun FieldSet.selectAll(): Query = Query(this, null)
見るとselectAll()
はあくまでQuery
というインスタンスを返しているに過ぎないようです。
ではQuery
の中を見てみましょう。
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
クラスそれ自体ではなんらかのクエリの呼び出しをしているわけではないということです。
では、スーパークラスでも同じことを確認しておきましょう。
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
クラスでも、コンストラクタや初期化ブロックがありません。
同じようにSizedIterable
とStatement
を見ておきます。
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
にて実装されています。
このことはあとで重要になってくるので、脳内メモリに刻んでおいてください。
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
}
}
みなさんの脳内メモリをお助けすると、今回の場合Statement
のtype
には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()
なのです。
では改めて中をよく見てみると…
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()
の中を見ていくことで、どのようにクエリが実行されているのか?を知ることができます。
ここでも皆さんの脳内メモリを補足しておくと、queryToExecute
はQuery
クラスでセットされています。
実態は、Statement
です。
このことを頭に置きながら、Transaction
のexec
メソッドを見ていきます。
クエリはどのように発行されるのか?
長々と書かれていますが、クエリを発行している実態はstmt.executeIn(this)
です。
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
メソッドの全体像は以下の通りなのですが、今回は要点を絞って確認します。
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
クラスです。
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
, describe
はTable
クラスの以下のメソッドです。
override fun describe(s: Transaction, queryBuilder: QueryBuilder): Unit = queryBuilder { append(s.identity(this@Table)) }
set.source.describe(transaction, this)
の結果としては、queryBuilder
にDEPARTMENT
が append されることになります。
クエリはどのように発行されているのか?
クエリ発行はstatement.executeInternal(transaction)
により行われていますが、この実態はQuery
クラスのexecuteInternal
メソッドです。
override fun PreparedStatementApi.executeInternal(transaction: Transaction): ResultSet? {
val fetchSize = this@Query.fetchSize ?: transaction.db.defaultFetchSize
if (fetchSize != null) {
this.fetchSize = fetchSize
}
return executeQuery()
}
ここで実行されるexecuteQuery
の実態は、JdbcPreparedStatementImpl
のexecuteQuery
メソッドです。
override fun executeQuery(): ResultSet = statement.executeQuery()
ここで先ほど組み立てられたクエリが発行され、結果セットを取得しているわけです。
結果セットはどのようにマッピングされているのか?
さて、クエリを組み立て、発行するまでがどうなっているかわかりました。
あと一息、結果セットがどのようにマッピングされているのか?が理解できれば、単一テーブルに対しての OR マッパーの仕組みを見切れます。
ところで、クエリ発行はAbstractQuery
クラスに定義されたiterator
メソッドで行われていました。
override fun iterator(): Iterator<ResultRow> {
val resultIterator = ResultIterator(transaction.exec(queryToExecute)!!)
return if (transaction.db.supportsMultipleResultSets) {
resultIterator
} else {
Iterable { resultIterator }.toList().iterator()
}
}
ここでResultIterator
というものを見ていきたいと思います。ResultIterator
はAbstractQuery
の内部クラスです。
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)
がマッピングの本体です。
ではResultRow
のcreate
メソッドについて見てみます。
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
}
}
}
//略
}
}
ResultRow
はfieldsIndex
とdata
をメンバ変数として持ちます。
fieldsIndex
はResultIterator
から受け取っており、この中に格納されているのはSlice
が持つTable
の列情報です。
これらを使いながらdata
はSlice
で指定した列順に値を Array<Any?>型で保持しています。
そう、あくまでResultRow
の内部では、値はインデックスで管理されているのです。
では、なぜ以下のように値へのアクセスが可能なのでしょうか?
val slice = Department.slice(Department.departmentId, Department.departmentName)
val selectAll = slice.selectAll().toList()
println(selectAll[0][Department.departmentId])
それはResultRow
のカスタムゲッターを見ることで理解できます。
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 コミュニティを作りました、ぜひご参加ください!!
また関西在住のソフトウェア開発者を中心に、関西エンジニアコミュニティを一緒に盛り上げてくださる方を募集しています。
よろしければ Conpass からメンバー登録よろしくお願いいたします。
Discussion