Exposedのキャッシュ周り挙動・内部実装について調べてみた
この記事は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)
されるようになっています
キャッシュが使われる時
キャッシュが使われるのは、下のいずれかのメソッドを使った時になります
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つの実装があります)
キャッシュの注意点
find
やall
ではキャッシュが使われない
実はよく使われるfind
メソッドではキャッシュが全く使われません
findとallの実装
find
でキャッシュを使いたい時はfindWithCacheCondition
を使いましょう
findWithCacheCondition
は使い方を間違えるとキケン
findWithCacheCondition
の実装を見ると、cacheCheckCondition
に1件でも引っかかるキャッシュがあれば、DBへの問い合わせは行いません
したがって、引数であるcacheCheckCondition
とop
の条件が同等になるように気をつけないと事故の元になるので要注意となります
おわりに
以上、Exposedのキャッシュ周りの挙動・内部実装についての知見を共有しました
最後までお読みいただきありがとうございました!
Discussion