iT邦幫忙

2021 iThome 鐵人賽

DAY 22
0
Modern Web

基於 Kotlin Ktor 建構支援模組化開發的 Web 框架系列 第 22

[Day 22] 實作 Database Plugin 整合 Exposed ORM, HikariCP 及 Flyway

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

點我連結到完整的 Database Plugin 程式碼

Exposed ORM

為什麼我選擇 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,我個人使用後非常推薦!

Database Plugin Configuration

我們先實作從外部設定檔 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.HikariConfigorg.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
)

Init HikariCP and Exposed

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)
    }
}

Flyway Migrate

當我們建立 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)
    }
}

完成結果


上一篇
[Day 21] 使用 Coroutine SendChannel 處理非同步工作
下一篇
[Day 23] 自定義 ColumnType, Operator, Expression 擴展 Exposed Query DSL API
系列文
基於 Kotlin Ktor 建構支援模組化開發的 Web 框架30

尚未有邦友留言

立即登入留言