前面我們聊到了如何存取資料庫,以及遇到 N+1 問題時該如何發現以及解決問題。
今天我們來談談 Exposed 框架如何非同步的存取資料。
在 Kotlin 程式語言中,支援透過協程(Coroutine)的方式操作,可以在不消耗大量資源的狀況下,達成非同步操作的需求。
可惜的是,由於 Exposed 框架的實作上,有一些記憶體儲存的位置,是儲存在線程內。如果協程在分配時,切換到不同的線程進行操作,可能會導致無法預期的問題。所以我們不能將同一個 transaction()
切換到不同的協程進行操作。
雖然同一個 transaction()
無法切換到不同協程,不過我們可以透過其他方式,來達成非同步的需求。
transaction()
外處理資料之前的範例內,我們都是在 transaction()
內處理完資料並印出內容。
transaction {
SchemaUtils.create(Users)
User.new {
name = "Alice"
}
User.new {
name = "Bob"
}
User
.all()
.forEach {
println("name: ${it.name}")
}
}
如果我們希望將資料傳輸到 transaction()
以外,我們可以在 transaction()
函數前面,用一個變數接收回傳值
val users = transaction {
SchemaUtils.create(Users)
User.new {
name = "Alice"
}
User.all().toList()
}
println(users.javaClass.kotlin)
users.forEach{
println(it.name)
}
這邊我們透過 toList()
,將原本的 User.all()
內容轉換成 java.util.ArrayList
類別。
執行這段程式之後,我們就可以看到 users
的類別和內容
class java.util.ArrayList
Alice
要注意的是,這段程式碼到現在還是同步執行的。如果我們對資料庫的存取很慢的話,會影響後面程式的運作。
我們用 java.lang.Thread.sleep
來模擬資料庫存取很耗時間時的情況。
val users = transaction {
sleep(3000)
SchemaUtils.create(Users)
User.new {
name = "Alice"
}
User.all().toList()
}
val users2 = transaction {
sleep(3000)
SchemaUtils.create(Users)
User.new {
name = "Bob"
}
User.all().toList()
}
val users3 = transaction {
sleep(3000)
SchemaUtils.create(Users)
User.new {
name = "Carol"
}
User.all().toList()
}
(users+users2+users3).forEach{
println(it.name)
}
這段程式可以成功的印出內容
Alice
Bob
Carol
但是運作起來要消費的時間很長,因為每段程式都必須要等前面的transaction()
程式執行完畢,也就是等三秒之後,才會往下執行。
也就是說,要跑完三次transaction()
,執行的時間至少需要花費九秒以上,才能執行完成。
suspendedTransactionAsync()
要讓程式能夠不被 sleep(3000)
卡住,先執行後面的部分,我們可以利用 suspendedTransactionAsync()
函數改寫我們的程式。
首先,將我們的 main()
宣告成 suspend
函數
suspend fun main()
再來,我們將原本的 transaction()
改寫成 suspendedTransactionAsync()
val users = suspendedTransactionAsync {
sleep(3000)
SchemaUtils.create(Users)
User.new {
name = "Alice"
}
User.all().toList()
}
val users2 = suspendedTransactionAsync {
sleep(3000)
SchemaUtils.create(Users)
User.new {
name = "Bob"
}
User.all().toList()
}
val users3 = suspendedTransactionAsync {
sleep(3000)
SchemaUtils.create(Users)
User.new {
name = "Carol"
}
User.all().toList()
}
這個函數回傳的內容,就不是我們之前所取得的 ArrayList
了。我們可以實際印出類別名稱看看
println(users.javaClass.kotlin)
會得到
class kotlinx.coroutines.DeferredCoroutine
並且我們執行時會發現到,印出println(users.javaClass.kotlin)
內容的時間變得很快。似乎沒有受到 sleep(3000)
的影響。
這是因為 suspendedTransactionAsync()
這個函數並沒有直接提供給我們和資料庫互動所取出的內容,而是先回傳一個 DeferredCoroutine
物件,程式就直接往下執行了。
要和資料庫互動,將 DeferredCoroutine
物件變成 ArrayList
物件,我們要透過 await()
函數來改寫我們的程式
(users.await()
+ users2.await()
+ users3.await()).forEach {
println(it.name)
}
這樣執行後,一樣可以取得我們的內容。並且由於這三段沒有互相等待對方執行的時間,所以執行時間會比起原先要短,不需要等九秒鐘以上才能執行完畢。