在串接資料庫的部分,Exposed 的用法已經在之前的 [Day 18] 資料表關聯,DAO 的一對多關係、[Day 19] 談談 n+1 問題和 eager loading、[Day 20] exposed 的多對多關聯 提過了,用法在這次改變上沒什麼差異。
不過這次 Exposed 還多了一個改變,很值得特別說一下。那就是除了最早支援的 JDBC,現在 Exposed 還支援 R2DBC 實作了。
Exposed 最早是設計在 JDBC 上,JDBC 的特色是同步、阻塞式,也就是查詢資料庫的時候會卡住一個線程。
但在 Ktor 這個原生支援非阻塞 I/O 的框架內,和其他非阻塞的程式放在一起時,一但效能需求提高時,這時候 JDBC 就會變成瓶頸。
這時候引用 R2DBC (Reactive Relational Database Connectivity),由於 R2DBC 本身的設計是非阻塞的,不會佔用一整個線程,所以特別適合高併發或 I/O 密集的場景。
我們來改寫看看之前的程式。首先我們加上對應的 gradle dependencies
我們先到 gradle/libs.versions.toml 調整 exposed 版本號碼
[versions]
exposed = "1.0.0-beta-5"
implementation("org.jetbrains.exposed:exposed-r2dbc:1.0.0-rc-1")
implementation("com.h2database:h2:2.2.224")
implementation("io.r2dbc:r2dbc-h2:1.0.0.RELEASE") // H2 R2DBC driver
接著我們將這段
val database = Database.connect(
url = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1",
user = "root",
driver = "org.h2.Driver",
password = "",
)
改寫成
val database = R2dbcDatabase.connect {
defaultMaxAttempts = 1
defaultR2dbcIsolationLevel = IsolationLevel.READ_COMMITTED
setUrl("r2dbc:h2:mem:///test;DB_CLOSE_DELAY=-1;")
}
UserService
內所有的引用也要跟著改變
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import org.jetbrains.exposed.v1.dao.IntEntity
import org.jetbrains.exposed.v1.dao.IntEntityClass
import org.jetbrains.exposed.v1.core.dao.id.EntityID
import org.jetbrains.exposed.v1.core.dao.id.IntIdTable
import org.jetbrains.exposed.v1.r2dbc.R2dbcDatabase
import org.jetbrains.exposed.v1.r2dbc.transactions.*
import org.jetbrains.exposed.v1.r2dbc.SchemaUtils
接著就是將 UserService
裡面阻塞的 transaction
都改成 suspendTransaction
了。
這邊要注意 init 的部分是阻塞的,在將裡面的 transaction
改成不阻塞的 suspendTransaction
時會出錯,由於這邊不是效能的主要瓶頸,這邊先簡單地用 runBlocking
包起來
init {
runBlocking {
suspendTransaction {
SchemaUtils.create(Users)
}
}
}
dbQuery
改成統一使用不會阻塞的 suspendTransaction
import org.jetbrains.exposed.v1.r2dbc.transactions.*
private suspend fun <T> dbQuery(block: suspend () -> T): T =
suspendTransaction(Dispatchers.IO) { block() }
今天的部分就到這邊,我們明天見!