3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

KotlinAdvent Calendar 2021

Day 14

[Kotlin][Exposed] Lazy Loading と Eager Loading - DaoにおけるSELECT文の実際

Last updated at Posted at 2021-12-14

概要

Kotlin製のORM Exposed にはDaoパターンの実装が用意されている。
このDao実装では、オブジェクトへの操作をフレームワークが自動でSQLに読み替える。
利用者はSQLをあまり意識することなく、データベース操作ができる。

ExposedのDao実装には Lazy Loading1Eager 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でいいのですけどね:thinking:

今回の調査コード全体

  1. Lazy LoadingはEager Loadingの逆にあたる非同期なロードを指す、本記事内の用語です。Exposed公式用語ではないことをご留意ください

  2. N+1問題とEager Loadingは、たとえばこちらの記事で簡単に解説されています

  3. 記事では扱いませんでしたが、当然Entityのプロパティの評価時もSQLの発行対象になります。また、単一のentityを取得するfindByIdのようなメソッドの実行時は、親のSQL発行は即時になるようでした。

3
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?