概要
Kotlin製のORM Exposed にはDaoパターンの実装が用意されている。
このDao実装では、オブジェクトへの操作をフレームワークが自動でSQLに読み替える。
利用者はSQLをあまり意識することなく、データベース操作ができる。
ExposedのDao実装には Lazy Loading1 と Eager Loading という2つのSELECT文の生成ストラテジーが存在する。
おもに親子関係をとるDaoにおいて提供されている選択肢で、デフォルトの挙動はLazy Loadingだ。
…せっかく使うなら、いつどんなSQLが生成されるか把握しておきたい。
- デフォルト(Lazy Loading)ではいつどんなSQLが生成されるか
- Eager Loadingにするとどう変わるのか
について、まとめてみる。
Exposed Daoの仕様
ExposedではDaoに**エンティティ(Entity)**という命名があてられている(以下、ExposedでのDaoをエンティティと呼ぶ)。
Exposedにおいて、親エンティティに子エンティティとの関係性を定義すると、親エンティティの取得にあわせて子エンティティも取得できるようになる。
// Daoでの親子関係の定義例
class ParentEntity(id: EntityID<Int>) : IntEntity(id) {
// ..中略
val children by ChildEntity referrersOn ChildTable.parent
}
親子関係のうち子がN件となるタイプについて、
Exposedが子エンティティの取得SQLを発行するタイミングは、デフォルトでは子エンティティの”初回利用時”(Lazy Loading)。子エンティティの取得SQLは、親エンティティの取得SQLと同時には発行されない。
一方のEager Loadingは、N+1問題の回避を目的として提供されているオプションだ。
ざっくりいえば、親エンティティの取得と同時での子エンティティの取得を強制する、という戦略になる2。
実際のコードでみる
前提
- 1-Nの親子関係をとるテーブルと、各テーブルに対応する**エンティティ(Entity)**を作成
-
ParentEntity
は親テーブルに対応し、ChildEntity
は子テーブルに対応
前提のコード
object ParentTable : IntIdTable("parent") {
val name = varchar("name", 50)
}
class ParentEntity(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<ParentEntity>(ParentTable)
var name by ParentTable.name
val children by ChildEntity referrersOn ChildTable.parent // 1-nということ
}
object ChildTable : IntIdTable("child") {
val parent = reference("parent_id", ParentTable)
val name = varchar("name", 50)
}
class ChildEntity(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<ChildEntity>(ChildTable)
var parent by ParentEntity referencedOn ChildTable.parent
var name by ChildTable.name
}
// SQLは省略
Lazy Loading
デフォルトの挙動。
実行コード
fun main() {
// ...前略
// DBに接続し、2組の親子レコードをinsertしておく
// insertした親レコードのidを持った状態でスタート
log.info { "start: ParentEntity.find(idList)" }
val parents = ParentEntity.find { ParentTable.id inList idList }
log.info { "end: ParentEntity.find(idList)" }
parents.forEach { parent ->
log.info { "start evaluation: parent.children" }
val children = parent.children // ChildEntity(ParentEntityと1-N関係をとる子エンティティ)
log.info { "end evaluation: parent.children $children" }
log.info { "start evaluation: children.first()" }
val child = children.first()
log.info { "end evaluation: children.first() $child" }
}
}
実行結果(出力ログ)
Exposedの生成SQLとそのタイミングは、ログレベルをDEBUGまで落とすと確認できる。
タイムスタンプのない改行やコメントは、出力ログに手で追記したもの。
※1
15:45:24.824 [main] INFO Logger - start: ParentEntity.find(idList)
15:45:24.831 [main] INFO Logger - end: ParentEntity.find(idList)
※2
15:45:24.841 [main] DEBUG Exposed - SELECT PARENT.ID, PARENT."NAME" FROM PARENT WHERE PARENT.ID IN (113, 114)
(ループ1週目)
※3
15:45:24.843 [main] INFO Logger - start evaluation: parent.children
15:45:24.847 [main] INFO Logger - end evaluation: parent.children
※4
15:45:24.848 [main] INFO Logger - start evaluation: children.first()
15:45:24.851 [main] DEBUG Exposed - SELECT CHILD.ID, CHILD.PARENT_ID, CHILD."NAME" FROM CHILD WHERE CHILD.PARENT_ID = 113
15:45:24.851 [main] INFO Logger - end evaluation: children.first()
(ループ2週目)
※3
15:45:24.852 [main] INFO Logger - start evaluation: parent.children
15:45:24.852 [main] INFO Logger - end evaluation: parent.children
※4
15:45:24.852 [main] INFO Logger - start evaluation: children.first()
15:45:24.852 [main] DEBUG Exposed - SELECT CHILD.ID, CHILD.PARENT_ID, CHILD."NAME" FROM CHILD WHERE CHILD.PARENT_ID = 114
15:45:24.853 [main] INFO Logger - end evaluation: children.first()
※1 ParentEntity.find(idList)
の呼び出し時点では、SQLは未発行
※2 親のListの評価時(parents.forEach()
の呼び出し時)に、親テーブル向けのSQLが発行
※3 子Entityのlistの評価(parent.children
)時点では、SQLは未発行
※4 各ループでの子Entityの評価時(children.first()
)に、子テーブルに対して都度SQLが発行
Eager Loading
Daoのselect系メソッドの呼び出しに#load
ないし#with
メソッドを後置すると、Exposedの挙動がEager Loadingになる。
実行コード
fun main() {
// ...前略。LazyLoadingの前提に同じ
log.info { "start: ParentEntity.find(idList)" }
val parents = ParentEntity.find { ParentTable.id inList idList }.with(ParentEntity::children) // ここだけちがう
log.info { "end: ParentEntity.find(idList)" }
parents.forEach { parent ->
log.info { "start evaluation: parent.children" }
val children = parent.children // ChildEntity(ParentEntityと1-N関係をとる子エンティティ)
log.info { "end evaluation: parent.children $children" }
log.info { "start evaluation: children.first()" }
val child = children.first()
log.info { "end evaluation: children.first() $child" }
}
}
実行結果(出力ログ)
16:02:07.440 [main] INFO Logger - start: ParentEntity.find(idList)
16:02:07.458 [main] DEBUG Exposed - SELECT PARENT.ID, PARENT."NAME" FROM PARENT WHERE PARENT.ID IN (115, 116)
16:02:07.520 [main] DEBUG Exposed - SELECT CHILD.ID, CHILD.PARENT_ID, CHILD."NAME" FROM CHILD WHERE CHILD.PARENT_ID IN (115, 116)
16:02:07.522 [main] INFO Logger - end: ParentEntity.find(idList)
16:02:07.522 [main] INFO Logger - start evaluation: parent.children
16:02:07.523 [main] INFO Logger - end evaluation: parent.children
16:02:07.523 [main] INFO Logger - start evaluation: children.first()
16:02:07.523 [main] INFO Logger - end evaluation: children.first()
16:02:07.523 [main] INFO Logger - start evaluation: parent.children
16:02:07.523 [main] INFO Logger - end evaluation: parent.children
16:02:07.523 [main] INFO Logger - start evaluation: children.first()
16:02:07.524 [main] INFO Logger - end evaluation: children.first()
-
ParentEntity.find(idList)
の呼び出し時点で、親子両テーブルに対してSQLが生成
(各ループでSQLは未発行)
##まとめ
- Lazy Loadingでは、親子ともにエンティティのインスタンスの初評価時にSQLが発行3
-
Eager Loadingでは、SELECT系メソッド(
find
等)の実行時にSQLが発行(エンティティのインスタンスの評価タイミングは関係なし)
#むすび
- N+1問題の回避/利便性 どちらを優先するケースもありえるとは思うので、オプションが用意されているのは親切ですね。
- Daoはそれなりに手軽に書けてうれしい反面、SQLが隠蔽されることの扱いづらさもひしひしと感じます。SQLの生成機構がスーパーインテリジェントなら、全てDaoでいいのですけどね