はじめに
技術評価としてkotlin
を試しています。
その中でORMとしてExposedを評価しました。
DSLとDAOの機能をそれぞれ試しましたが、
DAOを使用するうえでプライマリーキーに制約があったため、調査しました。
プライマリーキーに使用できる型には制限があり、Int, Long, UUIDの3種類のみになっています。
アプリケーションによりますが、任意の文字列をプライマリーキーとして使用したい場合の対応方法を調査、実装してみました。
DBの準備
docker-compose
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の依存関係
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
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を継承する必要があり、プライマリーキーの型毎に抽象クラスが用意されています。
文字列をプライマリーキーにするクラスはありません。
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を継承したエンティティクラスを作成する必要があり、プライマリーキーの型に応じて抽象クラスが用意されています。
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)
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)
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
カラムのみを決め打ちで条件にしています。
/**
* 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 ->
...
}
...
}