有了昨天介紹 SQLDelight 的基礎之下,讓我們繼續來看看怎麼使用它。
如果你還沒看過上一篇的話,請點這裡:
https://ithelp.ithome.com.tw/articles/10304793
我們所定義的 Player.sq
除了會在 MyDBImpl
裡插入 create table 的 command 之外,也會在相對應的 package 下建立相對應的 object class 以及他的 query class,以我們的例子就是 HockeyPlayer
跟 PlayerQueries
,不過點進去看以後應該會發現目前好像沒什麼特別的功能,如下:
// HockeyPlayer.kt
public data class HockeyPlayer(
public val player_number: Long,
public val full_name: String
) {
public override fun toString(): String = """
|HockeyPlayer [
| player_number: $player_number
| full_name: $full_name
|]
""".trimMargin()
}
// PlayerQueries.kt
public interface PlayerQueries : Transacter
而 MyDBImpl
裡也會有一小段相關程式碼如下:
private class PlayerQueriesImpl(
private val database: MyDBImpl,
private val driver: SqlDriver
) : TransacterImpl(driver), PlayerQueries
接下來回到我們一開始的問題,我們要怎麼在 SQLDelight 定義 function,讓我們可以動態的呼叫相對應的 function 來操作我們的 table 呢?
要回答這個問題還是需要回頭看我們的 Player.sq
,既然 SQLDelight 有辦法理解 sql command,最有可能的方式就是在這邊寫更多的 sql command 並給它個名字變成可呼叫的 function,而在 SQLDelight 裡定義一個 function 只要用冒號結尾,並接著 sql command 就可以了,範例如下:
selectAll:
SELECT *
FROM hockeyPlayer;
insert:
INSERT INTO hockeyPlayer(player_number, full_name)
VALUES (?, ?);
insertFullPlayerObject:
INSERT INTO hockeyPlayer(player_number, full_name)
VALUES ?;
把以上三段 sql 加到 Player.sq
並成功的 compile 之後,就會發現原本空的 PlayerQueries
現在變得跟 Room 所定義的 interface 有點像了,如下:
public interface PlayerQueries : Transacter {
public fun <T : Any> selectAll(mapper: (player_number: Long, full_name: String) -> T): Query<T>
public fun selectAll(): Query<HockeyPlayer>
public fun insert(player_number: Long, full_name: String): Unit
public fun insertFullPlayerObject(hockeyPlayer: HockeyPlayer): Unit
}
而具體的實作依然是放在
MyDBImpl
裡面,有興趣的大家可以自行看看~
現在的問題變成我們要怎麼使用呢?
這時候就是我們的 driver 登場的時候了!相信大家還記得這些 driver 是 platform dependent 的,所以我們可以運用之前所介紹的 expect/actual 技巧,定義如下:
// in src/commonMain
expect class DriverFactory {
expect fun createDriver(): SqlDriver
}
fun createDatabase(driverFactory): MyDB {
val driver = driverFactory.createDriver()
val database = MyDB(driver)
return database
}
// in src/androidMain
actual class DriverFactory(private val context: Context) {
actual fun createDriver(): SqlDriver {
return AndroidSqliteDriver(MyDB.Schema, context, "test.db")
}
}
// in src/iosMain
actual class DriverFactory {
actual fun createDriver(): SqlDriver {
return NativeSqliteDriver(MyDB.Schema, "test.db")
}
}
接下來在各 platform 裡建立 driverFactory
後,呼叫 createDatabase
就可以拿到 MyDB 的實體囉,而透過 database.playerQueries
就可以拿到我們的 query class,有這個物件就可以呼叫我們所定義好的 sql command 囉,範例如下:
// init here, can move to di
val driver = driverFactory.createDriver()
val database = MyDB(driver)
val playerQueries: PlayerQueries = database.playerQueries
println(playerQueries.selectAll().executeAsList())
// Prints [HockeyPlayer(15, "Ryan Getzlaf")]
playerQueries.insert(player_number = 10, full_name = "Corey Perry")
println(playerQueries.selectAll().executeAsList())
// Prints [HockeyPlayer(15, "Ryan Getzlaf"), HockeyPlayer(10, "Corey Perry")]
val player = HockeyPlayer(10, "Ronald McDonald")
playerQueries.insertFullPlayerObject(player)
其實在 multi-platform 裡使用 sql 說起來有點小複雜,各 platform dependent 的程式以及 sql 跟 Kotlin 間的轉換,還有不少 interface 跟 implementation 的結構,但這個設計其實很漂亮,相信多試幾次後大家一定沒問題的!
https://github.com/cashapp/sqldelight/
https://cashapp.github.io/sqldelight/
https://cashapp.github.io/sqldelight/multiplatform_sqlite/