iT邦幫忙

2021 iThome 鐵人賽

DAY 13
1
Modern Web

Kotlin 怎麼操作資料庫?談談 Kotlin Exposed 框架系列 第 13

[Day 13] 非同步的操作資料庫?談 suspendedTransactionAsync

前面我們聊到了如何存取資料庫,以及遇到 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)  
}

這樣執行後,一樣可以取得我們的內容。並且由於這三段沒有互相等待對方執行的時間,所以執行時間會比起原先要短,不需要等九秒鐘以上才能執行完畢。


上一篇
[Day 12] N+1 問題的解決方式:eager loading
下一篇
[Day 14] 更換連線的資料庫,聊 Database.connect 的操作
系列文
Kotlin 怎麼操作資料庫?談談 Kotlin Exposed 框架30

尚未有邦友留言

立即登入留言