search
LoginSignup
3

More than 1 year has passed since last update.

posted at

updated at

Organization

ExposedのDAOで任意の文字列をプライマリーキーとして使用する

はじめに

技術評価としてkotlinを試しています。
その中でORMとしてExposedを評価しました。

DSLDAOの機能をそれぞれ試しましたが、
DAOを使用するうえでプライマリーキーに制約があったため、調査しました。

プライマリーキーに使用できる型には制限があり、Int, Long, UUIDの3種類のみになっています。
アプリケーションによりますが、任意の文字列をプライマリーキーとして使用したい場合の対応方法を調査、実装してみました。

DBの準備

docker-compose

docker-compose.yml
version: '3'
services:
  mysql:
    image: mysql:8.0
    command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake
    ports:
      - "3306:3306"
    cap_add:
      - SYS_NICE  # CAP_SYS_NICE
    environment:
      MYSQL_DATABASE: testdb
      MYSQL_ROOT_USER: root
      MYSQL_ROOT_PASSWORD: root
      MYSQL_USER: test
      MYSQL_PASSWORD: test

gradleの依存関係

build.gradle.kts
dependencies {
    implementation("org.jetbrains.exposed:exposed-core:0.28.1")
    implementation("org.jetbrains.exposed:exposed-dao:0.28.1")
    implementation("org.jetbrains.exposed:exposed-jdbc:0.28.1")
    implementation("org.jetbrains.exposed:exposed-java-time:0.28.1")
    implementation("com.zaxxer:HikariCP:3.4.5")
    runtimeOnly("mysql", "mysql-connector-java", "8.0.21")
}

接続

接続プール

HikariCPを使用する。

import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource

object DatabaseFactory {
    fun create(url: String, driver: String, user: String, password: String): HikariDataSource {
        val config = HikariConfig()
        config.driverClassName = driver
        config.jdbcUrl = url
        config.username = user
        config.password = password
        config.maximumPoolSize = 3
        config.isAutoCommit = false
        config.validate()
        return HikariDataSource(config)
    }
}
val url = "jdbc:mysql://localhost:3306/testdb?characterEncoding=utf8&connectionCollation=utf8mb4_bin&useSSL=false"
val driver = "com.mysql.cj.jdbc.Driver"
val user = "test"
val pass = "test"
Database.connect(datasource = DatabaseFactory.create(url, driver, user, pass))

テーブル定義

DSLでもDAOでも、テーブル定義を作成する必要があります。

DSL

DSLの場合、Tableクラスを継承します。

import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.`java-time`.datetime

object ExampleTable: Table("examples") {
    val id = varchar("example_id", 255)
    val name = varchar("name", 255).nullable()
    val createdAt = datetime("created_at")
    val updatedAt = datetime("updated_at")
    val deletedAt = datetime("deleted_at").nullable()
    override val primaryKey = PrimaryKey(id)
}

DAO

DAOの場合、id というカラムを持つIdTableを継承する必要があり、プライマリーキーの型毎に抽象クラスが用意されています。
文字列をプライマリーキーにするクラスはありません。

org.jetbrains.exposed.dao.id.IdTable
abstract class IdTable<T:Comparable<T>>(name: String = ""): Table(name) {
    abstract val id : Column<EntityID<T>>
}

open class IntIdTable(name: String = "", columnName: String = "id") : IdTable<Int>(name) {
    override val id: Column<EntityID<Int>> = integer(columnName).autoIncrement().entityId()
    override val primaryKey by lazy { super.primaryKey ?: PrimaryKey(id) }
}

open class LongIdTable(name: String = "", columnName: String = "id") : IdTable<Long>(name) {
    override val id: Column<EntityID<Long>> = long(columnName).autoIncrement().entityId()
    override val primaryKey by lazy { super.primaryKey ?: PrimaryKey(id) }
}

open class UUIDTable(name: String = "", columnName: String = "id") : IdTable<UUID>(name) {
    override val id: Column<EntityID<UUID>> = uuid(columnName)
            .autoGenerate()
            .entityId()
    override val primaryKey by lazy { super.primaryKey ?: PrimaryKey(id) }
}

文字列をプライマリーキーとするクラスを作成する

StringIdTable

任意の文字列をプライマリーキーにするためのIdTableサブクラスを作成します

open class StringIdTable(name: String = "", columnName: String = "id", length: Int, defaultValueFun: (() -> String)? = null) : IdTable<String>(name) {
    override val id: Column<EntityID<String>> = if (defaultValueFun != null) {
        varchar(columnName, length).clientDefault( defaultValueFun).uniqueIndex().entityId()
    } else {
        varchar(columnName, length).uniqueIndex().entityId()
    }
    override val primaryKey by lazy { super.primaryKey ?: PrimaryKey(id) }
}
  • 継承例
object ExampleTable: StringIdTable("examples", "example_id", 255) {
    val name = varchar("name", 255).nullable()
    val createdAt = datetime("created_at")
    val updatedAt = datetime("updated_at")
    val deletedAt = datetime("deleted_at").nullable()
}

Entity

DAOの場合、Entityを継承したエンティティクラスを作成する必要があり、プライマリーキーの型に応じて抽象クラスが用意されています。

org.jetbrains.exposed.dao.IntEntity
abstract class IntEntity(id: EntityID<Int>) : Entity<Int>(id)
abstract class IntEntityClass<out E: IntEntity>(table: IdTable<Int>, entityType: Class<E>? = null) : EntityClass<Int, E>(table, entityType)
org.jetbrains.exposed.dao.LongEntity
abstract class LongEntity(id: EntityID<Long>) : Entity<Long>(id)
abstract class LongEntityClass<out E: LongEntity>(table: IdTable<Long>, entityType: Class<E>? = null) : EntityClass<Long, E>(table, entityType)
org.jetbrains.exposed.dao.UUIDEntity
abstract class UUIDEntity(id: EntityID<UUID>) : Entity<UUID>(id)
abstract class UUIDEntityClass<out E: UUIDEntity>(table: IdTable<UUID>, entityType: Class<E>? = null) : EntityClass<UUID, E>(table, entityType)

文字列をキーとする抽象エンティティクラスを作成する

StringEntity

abstract class StringEntity(id: EntityID<String>) : Entity<String>(id)
abstract class StringEntityClass<out E: StringEntity>(table: IdTable<String>, entityType: Class<E>? = null) : EntityClass<String, E>(table, entityType)
  • 継承例
class Examples(id: EntityID<String>) : StringEntity(id) {
    companion object : StringEntityClass<Examples>(ExampleTable)

    var name by ExampleTable.name
    var createdAt by ExampleTable.createdAt
    var updatedAt by ExampleTable.updatedAt
    var deletedAt by ExampleTable.deletedAt
}

文字列をプライマリーキーにするためのクラスを作成しました。

insert

DAOのinsertは newメソッドで行います。
テーブル定義のdefaultValueFunを設定していれば、都度生成することも可能ですが、設定しない場合はメソッドの引数にIDを渡す必要があります。

val id = "randomstring"
val now = LocalDateTime.now()
Examples.new(id){
    name = "onamae"
    createdAt = now
    updatedAt = now
}

まとめ

DAOには気になった制約がほかにもあり、複数カラムのプライマリーキーができません
EntityClassに定義されているfindByIdメソッドでは、id カラムのみを決め打ちで条件にしています。

org.jetbrains.exposed.dao.EntityClass
/**
* Get an entity by its [id].
*
* @param id The id of the entity
*
* @return The entity that has this id or null if no entity was found.
*/
open fun findById(id: EntityID<ID>): T? = testCache(id) ?: find{table.id eq id}.firstOrNull()

継承してオーバーライドするなど、提供されていない機能を独自に実装することは可能です。しかし、今後のバージョンと競合し、のちのちのバージョンで苦労する可能性も否定できません。

DSLを使うか、DAOを使うかは判断に迷うところではあるとおもいます。

  • 任意の文字列をプライマリーキーにする必要がある
  • 複数のカラムを組み合わせてプライマリーキーにする必要がある

このようなテーブルの場合にはDSLを使うというのが、一つの判断基準になるのではないでしょうか。

おまけ

raw SQL

for update nowaitを使いたくなった際に、DSLに機能を見つけられなかったため直接SQLを記述。

import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.transactions.TransactionManager

fun lock(id: String) {
  ...
  val args = listOf(Pair(ExampleTable.id.columnType as ColumnType, id))
  TransactionManager.current().exec("select * from examples where example_id = ? for update nowait", args) { rs ->
    ...
  }
  ...
}

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
3