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

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

ExposedのDao実装には **Lazy Loading**[^1] と **Eager Loading** という2つの*SELECT*文の生成ストラテジーが存在する。
おもに親子関係をとるDaoにおいて提供されている選択肢で、デフォルトの挙動は**Lazy Loading**だ。

…せっかく使うなら、いつどんな**SQL**が生成されるか把握しておきたい。

- デフォルト(**Lazy Loading**)ではいつどんなSQLが生成されるか
- **Eager Loading**にするとどう変わるのか

について、まとめてみる。

## Exposed Daoの仕様
ExposedではDaoに**エンティティ(Entity)**という命名があてられている(以下、ExposedでのDaoをエンティティと呼ぶ)。
Exposedにおいて、親エンティティに子エンティティとの関係性を定義すると、親エンティティの取得にあわせて子エンティティも取得できるようになる。

```kotlin
// 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問題**の回避を目的として[提供されているオプションだ](https://github.com/JetBrains/Exposed/wiki/DAO#eager-loading)。
ざっくりいえば、**親エンティティの取得と同時**での子エンティティの取得を強制する、という戦略になる[^2]。

# 実際のコードでみる
## 前提
- **1-N**の親子関係をとるテーブルと、各テーブルに対応する**エンティティ(Entity)**を作成
- `ParentEntity`は親テーブルに対応し、`ChildEntity`は子テーブルに対応

<details><summary>前提のコード</summary><div>

```kotlin
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は省略
```
</div></details>

## Lazy Loading
デフォルトの挙動。

#### 実行コード
```kotlin
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**になる。

#### 実行コード
```kotlin
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_face:

[今回の調査コード全体](https://github.com/satotats/kotlin-training/tree/master/src/main/kotlin/com/satotats/exercise/kotlin/exposed/loading)

[^1]: Lazy LoadingはEager Loadingの逆にあたる非同期なロードを指す、本記事内の用語です。Exposed公式用語ではないことをご留意ください
[^2]: N+1問題とEager Loadingは、たとえば[こちらの記事](https://qiita.com/shosho/items/abf6423283f761703d01#%E3%83%AA%E3%83%AC%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AE-eager-loading)で簡単に解説されています
[^3]: 記事では扱いませんでしたが、当然Entityのプロパティの評価時もSQLの発行対象になります。また、単一のentityを取得する`findById`のようなメソッドの実行時は、親のSQL発行は即時になるようでした。