🦑

ExposedでStatementInterceptorを使ってSQL文実行前後に処理を差し挟む

2024/12/09に公開

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

はじめに

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

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

本記事では、ExposedでSQL文実行前後に処理を差し挟むための機構であるStatementInterceptorの紹介とサンプルコードを共有します

StatementInterceptorとは

ExposedからRDBに対して発行されるSQL文(厳密にはStatementクラス)実行時の各ライフサイクルで処理を挟むためのInterfaceです

一般的な流れとしては

  1. beforeExecutionの実行
  2. prepared statementの作成
  3. afterStatementPreparedの実行
  4. SQLクエリの実行
  5. afterExecutionの実行

となります(他にbeforeCommitafterCommitbeforeRollbackafeterRollbackがあります)

https://github.com/JetBrains/Exposed/blob/main/exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/statements/StatementInterceptor.kt

使い方

基本的な使い方は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の紹介とサンプルコードの共有でした

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

参考資料

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

Discussion