Home About Contact
Kotlin Exposed , Kotlin Script

Kotlin Exposed その2, DSL と DAO

前回 雰囲気で Kotlin Exposed に入門した。 今回は復習をかねてもう少し理解を深めます。

環境:

$ kotlin -version
Kotlin version 2.1.0-release-394 (JRE 17.0.14+7-LTS)

Exposed DSL

出発点:

// exposed.main.kts

@file:Repository("https://repo1.maven.org/maven2/")

@file:DependsOn("org.jetbrains.exposed:exposed-core:0.59.0")
@file:DependsOn("org.jetbrains.exposed:exposed-jdbc:0.59.0")
@file:DependsOn("com.h2database:h2:2.3.232")

import org.jetbrains.exposed.sql.Table

data class Pokemon(val id: Int, val name: String)

object PokemonsTable : Table("pokemons") {
    val id = integer("id").autoIncrement()
    val name = varchar("name", length = 128)

    override val primaryKey = PrimaryKey(id)
}

データクラス Pokemon を定義して、それを管理するためのテーブルを用意。 テーブル名を pokemons として設定。

次にデータベースの準備:

...
import org.jetbrains.exposed.sql.Database
...

Database.connect(
    url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
    driver = "org.h2.Driver",
    user = "root",
    password = "",
)

次に実際にデータベース上に pokemons テーブルを作成:

...
import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.addLogger
import org.jetbrains.exposed.sql.StdOutSqlLogger
import org.jetbrains.exposed.sql.SchemaUtils
...

transaction {
    addLogger(StdOutSqlLogger)

    println("--- drop and create pokemons table ---")
    SchemaUtils.drop(PokemonsTable)
    SchemaUtils.create(PokemonsTable)
}

この段階で一度実行します。

$ kotlin exposed.main.kts
--- drop and create pokemons table ---
SQL: SELECT SETTING_VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE SETTING_NAME = 'MODE'
SQL: DROP TABLE IF EXISTS POKEMONS
SQL: CREATE TABLE IF NOT EXISTS POKEMONS (ID INT AUTO_INCREMENT PRIMARY KEY, "name" VARCHAR(128) NOT NULL)

Pikachu と Squirtle をテーブルに追加します。

...
import org.jetbrains.exposed.sql.insert
...

transaction {
    ...
    println("--- insert some pokemons ---")
    PokemonsTable.insert { it[name] = "Pikachu" }
    PokemonsTable.insert { it[name] = "Squirtle" }
}

本当に追加できているか確かめるために pokemons テーブルのアイテムを全部取得して標準出力します。

...
import org.jetbrains.exposed.sql.selectAll
...

transaction {
    ...
    println("--- listup all pokemons ---")
    val pokemons = PokemonsTable.selectAll()
    pokemons.forEach {
        println(it)
    }
}

それでは実行してみます。

$ kotlin exposed.main.kts
--- drop and create pokemons table ---
SQL: SELECT SETTING_VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE SETTING_NAME = 'MODE'
SQL: DROP TABLE IF EXISTS POKEMONS
SQL: CREATE TABLE IF NOT EXISTS POKEMONS (ID INT AUTO_INCREMENT PRIMARY KEY, "name" VARCHAR(128) NOT NULL)
--- insert some pokemons ---
SQL: INSERT INTO POKEMONS ("name") VALUES ('Pikachu')
SQL: INSERT INTO POKEMONS ("name") VALUES ('Squirtle')
--- listup all pokemons ---
SQL: SELECT POKEMONS.ID, POKEMONS."name" FROM POKEMONS
Exposed_main$PokemonsTable.id=1, Exposed_main$PokemonsTable.name=Pikachu
Exposed_main$PokemonsTable.id=2, Exposed_main$PokemonsTable.name=Squirtle

PokemonsTable.selectAll() して forEach した結果得られるインスタンスは org.jetbrains.exposed.sql.ResultRow であるらしい。

データベースから取得した... この ResultRow を データクラス Pokemon に変換する処理を追加します。

...
import org.jetbrains.exposed.sql.ResultRow
...

val toPokemon: (ResultRow)->Pokemon = {
    Pokemon(it[PokemonsTable.id], it[PokemonsTable.name])
}

...

transaction {
    ...

    val pokemons = PokemonsTable.selectAll().map(toPokemon)
    pokemons.forEach {
        println(it)
    }
}

それでは再度実行します。

$ kotlin exposed.main.kts
--- drop and create pokemons table ---
SQL: SELECT SETTING_VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE SETTING_NAME = 'MODE'
SQL: DROP TABLE IF EXISTS POKEMONS
SQL: CREATE TABLE IF NOT EXISTS POKEMONS (ID INT AUTO_INCREMENT PRIMARY KEY, "name" VARCHAR(128) NOT NULL)
--- insert some pokemons ---
--- listup all pokemons ---
Pokemon(id=1, name=Pikachu)
Pokemon(id=2, name=Squirtle)

これで意図通り作動しました。

ここまでのコード:

// exposed.main.kts

@file:Repository("https://repo1.maven.org/maven2/")

@file:DependsOn("org.jetbrains.exposed:exposed-core:0.59.0")
@file:DependsOn("org.jetbrains.exposed:exposed-jdbc:0.59.0")
@file:DependsOn("com.h2database:h2:2.3.232")

import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.ResultRow

import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.addLogger
import org.jetbrains.exposed.sql.StdOutSqlLogger
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.insert
import org.jetbrains.exposed.sql.selectAll

data class Pokemon(val id: Int, val name: String)

object PokemonsTable : Table("pokemons") {
    val id = integer("id").autoIncrement()
    val name = varchar("name", length = 128)

    override val primaryKey = PrimaryKey(id)
}

val toPokemon: (ResultRow)->Pokemon = {
    Pokemon(it[PokemonsTable.id], it[PokemonsTable.name])
}

Database.connect(
    url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
    driver = "org.h2.Driver",
    user = "root",
    password = "",
)

transaction {
    addLogger(StdOutSqlLogger)

    println("--- drop and create pokemons table ---")
    SchemaUtils.drop(PokemonsTable)
    SchemaUtils.create(PokemonsTable)

    println("--- insert some pokemons ---")
    PokemonsTable.insert { it[name] = "Pikachu" }
    PokemonsTable.insert { it[name] = "Squirtle" }

    println("--- listup all pokemons ---")
    val pokemons = PokemonsTable.selectAll()
    pokemons.map(toPokemon).forEach {
        println(it)
    }
}

Exposed DAO

それでは、DSL方式で書いてきたコードを DAOに変更していきます。

必要な依存を追加:

@file:DependsOn("org.jetbrains.exposed:exposed-dao:0.59.0")

必要なクラスをインポート:

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

pokemons テーブルの定義を DAO 用に変更:

//object PokemonsTable : Table("pokemons") {
//    val id = integer("id").autoIncrement()
//    val name = varchar("name", length = 128)
//
//    override val primaryKey = PrimaryKey(id)
//}

object PokemonsTable : IntIdTable("pokemons") {
    val name = varchar("name", 128)
}

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

PokemonsTable は IntIdTable を継承したオブジェクトとして定義しなおしました。 そして、肝は PokemonDAO でしょうか。 DAO するためのクラスを追加しています。

それではこの PokemonDAO を使ってDSL で操作したのと同じように DAO方式でポケモンを追加&出力してみます。

transaction {
    ...

    println("--- insert some pokemons ---")
    PokemonDAO.new { name = "Pikachu" }
    PokemonDAO.new { name = "Squirtle" }

    println("--- listup all pokemons ---")
    val pokemons = PokemonDAO.all()
    pokemons.forEach {
        println(it)
    }
}

実行します。

kotlin exposedDAO.main.kts
--- drop and create pokemons table ---
SQL: SELECT SETTING_VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE SETTING_NAME = 'MODE'
SQL: DROP TABLE IF EXISTS POKEMONS
SQL: CREATE TABLE IF NOT EXISTS POKEMONS (ID INT AUTO_INCREMENT PRIMARY KEY, "name" VARCHAR(128) NOT NULL)
--- insert some pokemons ---
--- listup all pokemons ---
SQL: INSERT INTO POKEMONS ("name") VALUES ('Pikachu')
SQL: INSERT INTO POKEMONS ("name") VALUES ('Squirtle')
SQL: SELECT POKEMONS.ID, POKEMONS."name" FROM POKEMONS
ExposedDAO_main$PokemonDAO@33c5e228
ExposedDAO_main$PokemonDAO@7d99846f

意図通り作動しました。 ただし、最後に出力されたのは PokemonDAO であって Pokemon データクラスではありません。 DAOを元の Pokemon クラスに戻す toPokemon 関数を追加:

val toPokemon: (PokemonDAO)->Pokemon = { Pokemon(it.id.value, it.name) }

あとは、この関数を適用すればOK:

    ...
    val pokemons = PokemonDAO.all().map(toPokemon)
    ...

再度実行:

$ kotlin exposedDAO.main.kts
...
Pokemon(id=1, name=Pikachu)
Pokemon(id=2, name=Squirtle)

Pokemon データクラスに変換した状態で出力できました。

DSL と DAO のコード差分:

--- exposed.main.kts    2025-01-29 20:30:47
+++ exposedDAO.main.kts    2025-01-29 20:31:36
@@ -1,35 +1,38 @@
-// exposed.main.kts
+// exposedDAO.main.kts
 
 @file:Repository("https://repo1.maven.org/maven2/")
 
 @file:DependsOn("org.jetbrains.exposed:exposed-core:0.59.0")
+@file:DependsOn("org.jetbrains.exposed:exposed-dao:0.59.0")
 @file:DependsOn("org.jetbrains.exposed:exposed-jdbc:0.59.0")
 @file:DependsOn("com.h2database:h2:2.3.232")
 
 import org.jetbrains.exposed.sql.Table
 import org.jetbrains.exposed.sql.Database
-import org.jetbrains.exposed.sql.ResultRow
 
 import org.jetbrains.exposed.sql.transactions.transaction
 import org.jetbrains.exposed.sql.addLogger
 import org.jetbrains.exposed.sql.StdOutSqlLogger
 import org.jetbrains.exposed.sql.SchemaUtils
-import org.jetbrains.exposed.sql.insert
-import org.jetbrains.exposed.sql.selectAll
 
+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
+
 data class Pokemon(val id: Int, val name: String)
 
-object PokemonsTable : Table("pokemons") {
-    val id = integer("id").autoIncrement()
-    val name = varchar("name", length = 128)
-
-    override val primaryKey = PrimaryKey(id)
+object PokemonsTable : IntIdTable("pokemons") {
+    val name = varchar("name", 128)
 }
 
-val toPokemon: (ResultRow)->Pokemon = {
-    Pokemon(it[PokemonsTable.id], it[PokemonsTable.name])
+class PokemonDAO(id: EntityID<Int>) : IntEntity(id) {
+    companion object : IntEntityClass<PokemonDAO>(PokemonsTable)
+    var name by PokemonsTable.name
 }
 
+val toPokemon: (PokemonDAO)->Pokemon = { Pokemon(it.id.value, it.name) }
+
 Database.connect(
     url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
     driver = "org.h2.Driver",
@@ -45,12 +48,12 @@
     SchemaUtils.create(PokemonsTable)
 
     println("--- insert some pokemons ---")
-    PokemonsTable.insert { it[name] = "Pikachu" }
-    PokemonsTable.insert { it[name] = "Squirtle" }
+    PokemonDAO.new { name = "Pikachu" }
+    PokemonDAO.new { name = "Squirtle" }
 
     println("--- listup all pokemons ---")
-    val pokemons = PokemonsTable.selectAll()
-    pokemons.map(toPokemon).forEach {
+    val pokemons = PokemonDAO.all().map(toPokemon)
+    pokemons.forEach {
         println(it)
     }
 }

まとめ

最後に完成した DAO 方式のコードを掲載:

// exposedDAO.main.kts

@file:Repository("https://repo1.maven.org/maven2/")

@file:DependsOn("org.jetbrains.exposed:exposed-core:0.59.0")
@file:DependsOn("org.jetbrains.exposed:exposed-dao:0.59.0")
@file:DependsOn("org.jetbrains.exposed:exposed-jdbc:0.59.0")
@file:DependsOn("com.h2database:h2:2.3.232")

import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.Database

import org.jetbrains.exposed.sql.transactions.transaction
import org.jetbrains.exposed.sql.addLogger
import org.jetbrains.exposed.sql.StdOutSqlLogger
import org.jetbrains.exposed.sql.SchemaUtils

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

data class Pokemon(val id: Int, val name: String)

object PokemonsTable : IntIdTable("pokemons") {
    val name = varchar("name", 128)
}

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

val toPokemon: (PokemonDAO)->Pokemon = { Pokemon(it.id.value, it.name) }

Database.connect(
    url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
    driver = "org.h2.Driver",
    user = "root",
    password = "",
)

transaction {
    addLogger(StdOutSqlLogger)

    println("--- drop and create pokemons table ---")
    SchemaUtils.drop(PokemonsTable)
    SchemaUtils.create(PokemonsTable)

    println("--- insert some pokemons ---")
    PokemonDAO.new { name = "Pikachu" }
    PokemonDAO.new { name = "Squirtle" }

    println("--- listup all pokemons ---")
    val pokemons = PokemonDAO.all().map(toPokemon)
    pokemons.forEach {
        println(it)
    }
}

追伸: suspendTransaction を使う

transaction の代わりに suspendTransaction を使ってみる。

これ:

import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction

run and forget 的な処理をする場合で処理順に依存しないケースでは transaction ではなく suspendTransaction を使えば処理が速くなるのかもしれない。

suspend するので、コルーチン環境を用意する必要があります。

おおざっぱな理解ではこんな感じになる:

...
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
...

runBlocking {
    launch {
        // ここで処理を実行(サスペンド関数を呼ぶ)
    }
}

newSuspendedTransaction を使ったデータベース操作処理用の関数を定義します。

ちなみに org/jetbrains/exposed/sql/transactions/experimental/Suspended.kt にある newSuspendedTransaction 関数の定義:

suspend fun <T> newSuspendedTransaction(
    context: CoroutineContext? = null,
    db: Database? = null,
    transactionIsolation: Int? = null,
    readOnly: Boolean? = null,
    statement: suspend Transaction.() -> T
): T =
    withTransactionScope(context, null, db, transactionIsolation, readOnly) {
        suspendedTransactionAsyncInternal(true, statement).await()
    }

ポケモン追加用:

suspend fun addPokemons(pokemonNames: List<String>) {
    pokemonNames.forEach { pokemonName->
        val block: Transaction.() -> Unit = {
            PokemonDAO.new { name = pokemonName } 
        }
        newSuspendedTransaction(Dispatchers.IO, statement = block)
    }
}

ポケモン名のリストを受け取って、そのポケモン名を持つポケモンをデータベースに追加。

データベースに存在するすべてのポケモンを取得:

suspend fun getAllPokemons(): List<Pokemon> {
    val block: Transaction.() -> List<Pokemon> = {
        PokemonDAO.all().map(toPokemon)
    }
    return newSuspendedTransaction(Dispatchers.IO, statement = block)
}

これが冗長すぎるのであれば、次のように suspendTransaction 関数を定義して、もう少し簡単に記述できる。

suspend fun <T> suspendTransaction(block: Transaction.() -> T): T {
    return newSuspendedTransaction(Dispatchers.IO, statement = block)
}

suspend fun addPokemons(pokemonNames: List<String>) {
    pokemonNames.forEach { pokemonName->
        suspendTransaction {
            PokemonDAO.new { name = pokemonName }
        }
    }
}
suspend fun getAllPokemons(): List<Pokemon> {
    return suspendTransaction {
        PokemonDAO.all().map(toPokemon)
    }
}

それでは、この2つの関数を runBlocking から呼び出します。

runBlocking {
    launch {
        addPokemons( listOf("Pikachu", "Squirtle") )
        getAllPokemons().forEach { println(it) }
    }
}

これで実行します。

$ kotlin exposedDAO2.main.kts
--- drop and create pokemons table ---
SQL: SELECT SETTING_VALUE FROM INFORMATION_SCHEMA.SETTINGS WHERE SETTING_NAME = 'MODE'
SQL: DROP TABLE IF EXISTS POKEMONS
SQL: CREATE TABLE IF NOT EXISTS POKEMONS (ID INT AUTO_INCREMENT PRIMARY KEY, "name" VARCHAR(128) NOT NULL)
Pokemon(id=1, name=Pikachu)
Pokemon(id=2, name=Squirtle)
StandaloneCoroutine{Completed}@2ca307d3

意図通りデータベースに追加したポケモンを全部出力できました。

もし、 runBlocking を次のように定義すると、意図通り作動しない:

runBlocking {
    launch {
        addPokemons( listOf("Pikachu", "Squirtle") )
    }

    launch {
        getAllPokemons().forEach { println(it) }
    }
}

(実行タイミングなどにも依存するかもしれませんが)追加と取得を別々の launch で定義すると、 ポケモンが出力されません。

これは newSuspendedTransaction を使っているため、 addPokemons をコールして処理が戻ってきた段階では、まだポケモンが追加されていない(追加されているとは限らない)からです。 runBlock に定義した2つの launch ブロックは並行処理になって、プログラマからは把握できないタイミングでその内容が実行されます。

データ追加や取得の処理順に依存するコードを書く場合には注意が必要です。

runBlocking の代わりに コールチンスコープをつくる場合

このように:

...
import kotlinx.coroutines.CoroutineScope
...

val scope = CoroutineScope(Dispatchers.IO)

scope.launch {
    addPokemons( listOf("Pikachu", "Squirtle") )
    getAllPokemons().forEach { println(it) }
}

Thread.sleep(1000)

これも、 newSuspendedTransaction を使って処理しているため、 最後の Thread.sleep(1000) で即座に終了するのを阻止しておかないと、 scope.launch ブロック内の処理が実行される前にプログラムが終了して結局ポケモンは出力されません。

完成したコード

// exposedDAO2.main.kts

@file:Repository("https://repo1.maven.org/maven2/")

@file:DependsOn("org.jetbrains.exposed:exposed-core:0.59.0")
@file:DependsOn("org.jetbrains.exposed:exposed-dao:0.59.0")
@file:DependsOn("org.jetbrains.exposed:exposed-jdbc:0.59.0")
@file:DependsOn("com.h2database:h2:2.3.232")

import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.Database

import org.jetbrains.exposed.sql.transactions.transaction

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking

import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.transactions.experimental.newSuspendedTransaction

import org.jetbrains.exposed.sql.addLogger
import org.jetbrains.exposed.sql.StdOutSqlLogger
import org.jetbrains.exposed.sql.SchemaUtils

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

data class Pokemon(val id: Int, val name: String)

object PokemonsTable : IntIdTable("pokemons") {
    val name = varchar("name", 128)
}

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

val toPokemon: (PokemonDAO)->Pokemon = { Pokemon(it.id.value, it.name) }

/*
suspend fun addPokemons(pokemonNames: List<String>) {
    pokemonNames.forEach { pokemonName->
        val block: Transaction.() -> Unit = {
            PokemonDAO.new { name = pokemonName } 
        }
        newSuspendedTransaction(Dispatchers.IO, statement = block)
    }
}

suspend fun getAllPokemons(): List<Pokemon> {
    val block: Transaction.() -> List<Pokemon> = {
        PokemonDAO.all().map(toPokemon)
    }
    return newSuspendedTransaction(Dispatchers.IO, statement = block)
}
*/

suspend fun <T> suspendTransaction(block: Transaction.() -> T): T {
    return newSuspendedTransaction(Dispatchers.IO, statement = block)
}

suspend fun addPokemons(pokemonNames: List<String>) {
    pokemonNames.forEach { pokemonName->
        suspendTransaction {
            PokemonDAO.new { name = pokemonName }
        }
    }
}
suspend fun getAllPokemons(): List<Pokemon> {
    return suspendTransaction {
        PokemonDAO.all().map(toPokemon)
    }
}


Database.connect(
    url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
    driver = "org.h2.Driver",
    user = "root",
    password = "",
)

transaction {
    addLogger(StdOutSqlLogger)

    println("--- drop and create pokemons table ---")
    SchemaUtils.drop(PokemonsTable)
    SchemaUtils.create(PokemonsTable)
}

val scope = CoroutineScope(Dispatchers.IO)
scope.launch {
    addPokemons( listOf("Pikachu", "Squirtle") )
    getAllPokemons().forEach { println(it) }
}

Thread.sleep(1000)

/*
runBlocking {
    launch {
        addPokemons( listOf("Pikachu", "Squirtle") )
        getAllPokemons().forEach { println(it) }
    }
}
*/

以上です。