ExposedでStatementInterceptorを使ってSQL文実行前後に処理を差し挟む
この記事はKotlin Advent Calendar 2024 9日目の記事です
はじめに
こんにちは、株式会社スマートラウンドの@tsukakei1012です!
当社では、サーバーサイドKotlinで開発しており、ORマッパーとしてJetBrainsのExposedを利用しています
本記事では、ExposedでSQL文実行前後に処理を差し挟むための機構であるStatementInterceptorの紹介とサンプルコードを共有します
StatementInterceptorとは
ExposedからRDBに対して発行されるSQL文(厳密にはStatementクラス)実行時の各ライフサイクルで処理を挟むためのInterfaceです
一般的な流れとしては
-
beforeExecution
の実行 - prepared statementの作成
-
afterStatementPrepared
の実行 - SQLクエリの実行
-
afterExecution
の実行
となります(他にbeforeCommit
・afterCommit
・beforeRollback
・afeterRollback
があります)
使い方
基本的な使い方はStatementInterceptor
を継承 & 処理を挟みたい箇所の実装し、実装したInterceptorを登録するだけとなります
単一のトランザクション内でのInterceptorの登録・解除にはregisterInterceptor()
・unregisterInterceptor()
を使います
全てのトランザクションについて適用したい場合
GlobalStatementInterceptor
を継承した上で、resources以下にMETA-INF/services/org.jetbrains.exposed.sql.statements.GlobalStatementInterceptor
の新ファイルを用意します
ファイルの内容はGlobalStatementInterceptor
を継承したクラスのfull qualified class nameを記載します
サンプルコード
ここでは簡単な例としてDELETE文発行時に、実行前後でログを出力してみます
import org.jetbrains.exposed.dao.IntEntity
import org.jetbrains.exposed.dao.IntEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.statements.*
import org.jetbrains.exposed.sql.statements.api.PreparedStatementApi
import org.jetbrains.exposed.sql.transactions.transaction
object Cities: IntIdTable() {
val name = varchar("name", 50)
val code = varchar("code", 50).nullable().clientDefault { null }
}
class City(id: EntityID<Int>) : IntEntity(id) {
companion object : IntEntityClass<City>(Cities)
var name by Cities.name
var code by Cities.code
}
class AuditInterceptor : GlobalStatementInterceptor {
override fun beforeExecution(transaction: Transaction, context: StatementContext) {
if (context.statement.type == StatementType.DELETE) {
println("***** DELETING: ${context.expandArgs(transaction)} *****")
}
}
override fun afterExecution(
transaction: Transaction,
contexts: List<StatementContext>,
executedStatement: PreparedStatementApi
) {
contexts.forEach { context ->
if (context.statement.type == StatementType.DELETE) {
println("***** DELETED: ${context.expandArgs(transaction)} *****")
}
}
}
}
fun main() {
Database.connect("jdbc:h2:mem:test", driver = "org.h2.Driver", user = "root", password = "")
val interceptor = AuditInterceptor()
transaction {
registerInterceptor(interceptor)
SchemaUtils.create(Cities)
val tokyo = City.new { name = "Tokyo" }
val osaka = City.new { name = "Osaka" }
tokyo.delete() // AuditInterceptorによってログが出力される
unregisterInterceptor(interceptor)
osaka.delete() // ログが出力されない
}
}
出力結果
***** DELETING: DELETE FROM CITIES WHERE CITIES.ID = 1 *****
***** DELETED: DELETE FROM CITIES WHERE CITIES.ID = 1 *****
おわりに
以上、StatementInterceptorの紹介とサンプルコードの共有でした
最後までお読みいただきありがとうございました!
参考資料
- Exposed公式ドキュメント
- [Exploring the Exposed Library: A Kotlin Solution to Database Access from Kotlin Conf 2024] (https://kotlinconf.com/2024/talks/586918/)
Discussion