12
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

OPENLOGIAdvent Calendar 2020

Day 3

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

Last updated at Posted at 2020-12-02

はじめに

技術評価として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 ->
    ...
  }
  ...
}
12
5
0

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
  3. You can use dark theme
What you can do with signing up
12
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?