Java Web 框架通常都至少整合一種 ORM,只要 Gradle depenency 加一下,再到設定檔填入資料庫連線設定即可。但目前 Ktor 官方尚未整合任何一種 ORM,Github 也沒有找到套件,所以必須自己做 Database Plugin 調用 ORM API 進行整合。除了 ORM 之外,Connection Pool 及 Migration Tool 也是必要的基本功能,所以今天我先說明為何選擇 Exposed ORM,然後再實作 Database Plugin 整合 Exposed, HikariCP, Flyway 至 Ktor
為什麼我選擇 Exposed ORM 呢? 那是因為我從最早使用 Spring 搭配 Hibernate & JPA,到後來使用 Play Framework 搭配 Ebean,逐漸走向輕量化 Web 及 ORM 框架,所以對於 Ktor 要搭配那個 ORM,我想選擇與 Ktor 一樣由 JetBrains 開發的 100% Kotlin 及 typesafe SQL 的 Exposed。不要看到目前 Exposed 的版本號是 0.35.1 就認為還不成熟穩定,官方 2018 年在 github issue #359 回答「JebBrains 已經在 production 使用超過3年了」,所以換算今年 2021 已經超過6年了。
我覺得 Exposed 的優點是充分發揮 Kotlin 的特性,可以寫出簡潔 typesafe DSL 風格的 SQL,而且非常輕量易擴展。所以如果你使用 Kotlin 語言開發,又不需要用到一般 ORM 框架的進階功能,不妨考慮一下 Exposed,我個人使用後非常推薦!
我們先實作從外部設定檔 application.conf 讀取資料庫連線設定
database {
hikari {
driverClassName = "org.postgresql.Driver"
jdbcUrl = ${?DB_URL} #"jdbc:postgresql://localhost:5432/fanpoll"
username = ${?DB_USER}
password = ${?DB_PASSWORD}
minimumIdle = 10
maximumPoolSize = 20
idleTimeout = 600000
connectionTimeout = 10000
}
flyway {
baselineOnMigrate = true
validateOnMigrate = true
}
}
對應的 config data class 如下,之後要再轉為 com.zaxxer.hikari.HikariConfig
及 org.flywaydb.core.api.configuration.FluentConfiguration
物件。因為我只需要設定重要的屬性值,所以就沒有完整實作 HikariCP 及 Flyway 的所有設定了。
※ Exposed 在 2021-09-23 最新釋出的 0.35.1 版本增加 DatabaseConfig 類別,往後我們可以更方便設定 org.jetbrains.exposed.sql.Database
物件
data class DatabaseConfig(
val hikari: HikariConfig,
val flyway: FlywayConfig,
)
data class HikariConfig(
val driverClassName: String,
val jdbcUrl: String,
val username: String,
val password: String,
val minimumIdle: Int,
val maximumPoolSize: Int,
val idleTimeout: Long,
val connectionTimeout: Long
)
data class FlywayConfig(
val baselineOnMigrate: Boolean = true,
val validateOnMigrate: Boolean = true,
val table: String? = null
)
Database Plugin 讀取設定值後,先根據 HikariConfig
初始化 HikariDataSource
,再呼叫 ExposedDatabase.connect(dataSource)
建立 org.jetbrains.exposed.sql.Database
物件即可開始操作資料庫。最後不要忘記加上 KoinApplicationShutdownManager.register { closeConnection() }
,在停止 Server 時關閉資料庫連線。
private lateinit var dataSource: HikariDataSource
private lateinit var defaultDatabase: org.jetbrains.exposed.sql.Database
private fun connect(config: com.zaxxer.hikari.HikariConfig) {
try {
logger.info("===== connect database ${config.jdbcUrl}... =====")
dataSource = HikariDataSource(config)
defaultDatabase = ExposedDatabase.connect(dataSource)
logger.info("===== database connected =====")
} catch (e: Throwable) {
throw InternalServerException(
InfraResponseCode.DB_ERROR,
"fail to connect database connection pool! => ${config.jdbcUrl}", e
)
}
}
private fun closeConnection() {
try {
if (dataSource.isRunning) {
logger.info("close database connection pool...")
dataSource.close()
logger.info("database connection pool closed")
} else {
logger.warn("database connection pool had been closed")
}
} catch (e: Throwable) {
throw InternalServerException(InfraResponseCode.DB_ERROR, "could not close database connection pool", e)
}
}
當我們建立 HikariDataSource
物件之後,就可以根據 Flyway FluentConfiguration
物件建立 Flyway
物件,然後再呼叫 migrate()
方法執行 migrate 操作。總之有了 Flyway 物件之後,你就可以使用呼叫 API 的方式,執行 baseline
, clean
, undo
…等指令。
private lateinit var flyway: Flyway
private fun migrate(config: FluentConfiguration) {
try {
logger.info("===== Flyway migrate... =====")
flyway = config.dataSource(dataSource).load()
flyway.migrate()
logger.info("===== Flyway migrate finished =====")
} catch (e: Throwable) {
throw InternalServerException(InfraResponseCode.DB_ERROR, "fail to migrate database", e)
}
}