🦑

Exposedのキャッシュ周り挙動・内部実装について調べてみた

2024/12/03に公開

この記事はKotlin Advent Calendar 2024の3日目の記事です

TL;DR

  • Exposedのキャッシュは基本的にfun findById(id: EntityID<ID>): T?の時しか使われない
  • findでキャッシュを使いたい時はfindWithCacheConditionを使う

はじめに

こんにちは、株式会社スマートラウンド@tsukakei1012です!

当社では、サーバーサイドKotlinで開発しており、ORマッパーとしてJetBrainsのExposedを利用しています

本記事では、Exposedのキャッシュ周りの挙動・内部実装についての知見を共有します

登場人物(クラス)

  • Table: DBのテーブルに相当するクラス
  • Entity: DBテーブル内のレコードに相当するクラス
  • EntityClass: Entityクラスのインスタンス管理を行うクラス
  • EntityCache: 特定トランザクション内にあるEntityクラスのインスタンスのキャッシュを保存するクラス

サンプルコード

import org.jetbrains.exposed.dao.*
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.transaction

object Cities: IntIdTable() {
    val name = varchar("name", 50)
}

class City(id: EntityID<Int>) : IntEntity(id) {
    companion object : IntEntityClass<City>(Cities)
    var name by Cities.name
}

fun main() {
    Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver", user = "root", password = "")

    transaction {
        addLogger(StdOutSqlLogger)
        SchemaUtils.create(Cities)
        City.new { name = "Tokyo" }
        City.new { name = "Osaka" }

        City.findById(1)
        // -> SQL: SELECT CITIES.ID, CITIES."name" FROM CITIES WHERE CITIES.ID = 1
        City.findById(1) // キャッシュが効く
        City.findWithCacheCondition({ this.id.value == 1 }) { Cities.id eq 1 }.toList() // キャッシュが効く

        City.find { Cities.id eq 2 }.toList()
        // -> SQL: SELECT CITIES.ID, CITIES."name" FROM CITIES WHERE CITIES.ID = 2
        City.findById(2) // キャッシュが効く
        

        City.all().toList()
        // -> SQL: SELECT CITIES.ID, CITIES."name" FROM CITIES
        // キャッシュが効かない
        City.find { Cities.id eq 2 }.toList()
        // -> SQL: SELECT CITIES.ID, CITIES."name" FROM CITIES WHERE CITIES.ID = 2
        // 同じ条件でもキャッシュが効かない

        println(City.findWithCacheCondition({ this.id.value == 1 }) { Cities.id eq 2 }.toList().first().id)
        // 1
        // cacheCheckConditionとopの条件が違うため欲しいEntityが取れていない
    }
}

キャッシュが生成される時

キャッシュが生成されるのは、SELECT文が発行され、その結果であるResultRowからEntityクラスを生成するときとなります

いくつかメソッドはありますが、どれも最終的には下のfun wrap(id: EntityID<ID>, row: ResultRow?): Tが呼ばれて、warmCache().store(this, new)されるようになっています

https://github.com/JetBrains/Exposed/blob/0.56.0/exposed-dao/src/main/kotlin/org/jetbrains/exposed/dao/EntityClass.kt#L340-L351

キャッシュが使われる時

キャッシュが使われるのは、下のいずれかのメソッドを使った時になります

  • open fun findById(id: EntityID<ID>): T?
  • open fun forEntityIds(ids: List<EntityID<ID>>): SizedIterable<T>
  • fun findWithCacheCondition(cacheCheckCondition: T.() -> Boolean, op: SqlExpressionBuilder.() -> Op<Boolean>): Sequence<T>
各メソッドの実装

具体的な実装としては、各メソッドの実装内でtestCacheメソッドが呼ばれており、それでキャッシュヒットしたときとなります
(ちなみにtestCacheにはfun testCache(id: EntityID<ID>): T?fun testCache(cacheCheckCondition: T.() -> Boolean): Sequence<T>の2つの実装があります)

キャッシュの注意点

findallではキャッシュが使われない

実はよく使われるfindメソッドではキャッシュが全く使われません

findとallの実装

findでキャッシュを使いたい時はfindWithCacheConditionを使いましょう

findWithCacheConditionは使い方を間違えるとキケン

findWithCacheConditionの実装を見ると、cacheCheckConditionに1件でも引っかかるキャッシュがあれば、DBへの問い合わせは行いません

したがって、引数であるcacheCheckConditionopの条件が同等になるように気をつけないと事故の元になるので要注意となります

おわりに

以上、Exposedのキャッシュ周りの挙動・内部実装についての知見を共有しました

最後までお読みいただきありがとうございました!

スマートラウンド テックブログ

Discussion