はじめに
ゲーム・アニメ事業部でスマートフォンゲームのサーバサイド開発をしている山根です。
現在の担当しているタイトルでは、Kotlinを使ってサーバサイドを開発しており、
フレームワークはKtor、ORMはExposedを使っています。
今回は、ORMであるExposedを使う上で工夫した点をお話しさせていただきます。
Exposedについて
Exposedは、プログラミング言語であるKotlinを開発したJetBrainsが開発した純Kotlin製のORMです。
また、今回話には出ませんが、KtorもJetBrainsが開発した純Kotlin製のフレームワークになります。
github.com
工夫した点
DBの接続先の切り替え
処理中にDBの接続先を切り替える必要があり、実装を検討してみました。
まず、Exposedではtransaction
メソッドを使うことでDBへ接続してSQLを実行することができます。
transaction { // DSL/DAO operations go here }
ですが、そのままだとデフォルトのDatabase
へ接続してしまいます。
Exposedのドキュメントによると、transaction
引数に接続先を表すDatabase
のインスタンスを指定することができるので、
その切り替えを実現することが出来ると記載されています。
val newDatabase: Database = Database.connect("url", "driver", "user", "password") transaction(db = newDatabase) { // DSL/DAO operations go here }
github.com 上記のコードを元に、複数の接続先の切り替えができるようにしてみます。
// 接続先をenvKeyを使って切り替える fun transactionByEnvKey(envKey: String, block: () -> Unit): { val targetDB = when (envKey) { "Env1" -> Database.connect("env1-url", "env1-driver", "env1-user", "env1-password"), "Env2" -> Database.connect("env2-url", "env2-driver", "env2-user", "env2-password"), .... else -> Database.connect("else-url", "else-driver", "else-user", "else-password") } transaction(db = targetDB) { block() } } transactionByEnvKey("Env1") { // DSL/DAO operations go here }
transactionByEnvKey
は、指定のenvKey
によって接続先を切り替えることができる関数になります。
このままでも、接続先の切り替えはできますが、さらに工夫していきます。
まず、envKey
とDatabase
を管理するクラスDatabaseConnection
を用意し、
さらに、DatabaseConnection
を複数管理するためにDatabaseConnections
を用意します。
data class DatabaseConnections(private val databaseConnections: List<DatabaseConnection>) { fun getConnection(envKey: String) = databaseConnections.first { it.envKey == envKey } } class DatabaseConnection(val envKey: String, override val database: Database): DatabaseConnectionManager interface DatabaseConnectionManager { val database: Database fun <T> withConnection(block: () -> T): T { return transaction(db = database){ block() } } }
DatabaseConnections
のgetConnection
は、DatabaseConnection
の配列からenvKey
を指定することで、
指定のDatabaseConnection
を取得することができます。
これを使い、接続先の切り替えを行ってみます。
val env1DatabaseConnection = DatabaseConnection("Env1", Database.connect("env1-url", "env1-driver", "env1-user", "env1-password")) val env2DatabaseConnection = DatabaseConnection("Env2", Database.connect("env2-url", "env2-driver", "env2-user", "env2-password")) .... val elseDatabaseConnection = DatabaseConnection("else", Database.connect("else-url", "else-driver", "else-user", "else-password")) val allConnections: DatabaseConnections = DatabaseConnections(listOf(env1DatabaseConnection, env2DatabaseConnection, ...., elseDatabaseConnection)) allConnections.getConnection("Env1").withConnection { // DSL/DAO operations go here }
上記のようにすることで、DatabaseConnections
を使った接続先の切り替えができました。
また、このように複数のDatabaseConnection
をDatabaseConnections
のようなクラスに置き、依存注入することで、
DatabaseConnection
のインスタンスの生成頻度を抑えつつ、envKey
を指定するだけで切り替えることができます。
created_at, updated_atをinsert, update時に共通で入れる方法
一般的にテーブル定義には、created_at, updated_atカラムを用意していることが多いと思いますが、 このカラムをinsert, updateの処理で、毎回記述するのはちょっとなと思い、 created_at, updated_atを意識しないinsert, updateの処理を用意してみました。
コード例
import org.jetbrains.exposed.dao.id.IntIdTable import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Table import org.jetbrains.exposed.sql.javatime.datetime import org.jetbrains.exposed.sql.statements.InsertStatement import org.jetbrains.exposed.sql.transactions.TransactionManager import java.time.LocalDateTime interface BaseModel { val createdAt: LocalDateTime val updatedAt: LocalDateTime } open class BaseEntity<M: BaseModel>: IntIdTable() { val createdAt: Column<LocalDateTime> by lazy { datetime("created_at") } val updatedAt: Column<LocalDateTime> by lazy { datetime("updated_at") } } interface BaseRepository<M : BaseModel, Entity : BaseEntity<M>> { val entity: Entity fun <T : Table> T.insert(body: T.(InsertStatement<Number>) -> Unit) { val now = LocalDateTime.now() InsertStatement<Number>(this).apply { this[entity.createdAt] = now this[entity.updatedAt] = now body(this) execute(TransactionManager.current()) } } fun <T : Table> T.update( where: Op<Boolean>? = null, limit: Int? = null, body: T.(UpdateStatement) -> Unit, ) { val now = LocalDateTime.now() UpdateStatement(this, limit, where).apply { this[entity.updatedAt] = now body(this) execute(TransactionManager.current()) } } }
上記のように用意すれば、insert, updateの処理の共通処理を実装することができ、以下のような感じで実装できます。
data class User( override val id: EntityID<Int>, val name: String, val age: Int, override val createdAt: LocalDateTime, override val updatedAt: LocalDateTime ): BaseModel object Users : BaseEntity<User>() { val name = varchar("name", 64) val age = integer("age") } class UsersRepository: BaseRepository<User, Users> { override val entity = Users fun insert(name: String, age: Int) { entity.insert { it[this.name] = name it[this.age] = age } } fun update(age: Int) { entity.update(entity.name.eq("test")) { it[this.age] = age } } }
おまけ
Exposedでは、SQLのBulkInsertに変換するためのbatchInsert
という関数が用意されています。
先ほどのコード例に寄せたサンプルコードが以下になります。
fun <T : Table, E> T.batchInsert( data: Iterable<E>, shouldReturnGeneratedValues: Boolean, body: BatchInsertStatement.(E) -> Unit, ) { val now = LocalDateTime.now() val customBody: BatchInsertStatement.(E) -> Unit = { this.apply { this[entity.createdAt] = now this[entity.updatedAt] = now body(it) } } batchInsert( data, ignore = false, shouldReturnGeneratedValues = shouldReturnGeneratedValues, customBody ) }
これにより、BulkInsertでもcreated_at, updated_atを意識しない記述をすることができます。