如果只會單一資料表的 CRUD 操作,那麼有很多需求是沒有辦法滿足的。
今天我們來聊聊,怎麼用 DSL 的方式進行其他的操作。
如果我們要一次寫入多筆資料,我們可以透過 batchInsert()
這樣處理
val cityNames = listOf("Paris", "Moscow", "Helsinki")
cities
.batchInsert(cityNames) { name ->
this[cities.name] = name
}
寫入之後,我們看看資料是否存在於資料庫裡面
Cities
.selectAll()
.forEach {
println("City #${it[Cities.id]}: ${it[Cities.name]}")
}
這樣我們就可以看到
City #1: Paris
City #2: Moscow
City #3: Helsinki
我們一開始宣告的資料,成功寫在資料庫裡面了
如果我們有限制需要取出資料的筆數,我們可以用 limit()
函數,搭配 offset
參數取出資料
Cities
.selectAll()
.limit(2, 1)
.forEach {
println("City #${it[Cities.id]}: ${it[Cities.name]}")
}
可以看到
City #2: Moscow
City #3: Helsinki
我們可以用 orderBy()
函數,幫我們對取出的資料進行排序
Cities
.selectAll()
.orderBy(Cities.name to SortOrder.ASC)
.forEach {
println("City #${it[Cities.id]}: ${it[Cities.name]}")
}
執行後,可以看到我們的資料順序改變了
City #3: Helsinki
City #2: Moscow
City #1: Paris
如果我們有多張資料表,像是
object Cities : IntIdTable() {
val name = varchar("name", 50)
}
object Users : IntIdTable() {
val cityId = integer("cityId")
val name = varchar("name", 50)
}
並且資料表的內容如下
SchemaUtils.create(Cities)
SchemaUtils.create(Users)
val cityNames = listOf("Paris", "Moscow", "Helsinki")
val users = listOf(
mapOf("name" to "Alice", "cityId" to "1"),
mapOf("name" to "Bob", "cityId" to "2"),
mapOf("name" to "Carol", "cityId" to "2"),
mapOf("name" to "Dave", "cityId" to "3"),
mapOf("name" to "Eve", "cityId" to "3"),
)
Cities
.batchInsert(cityNames) { name ->
this[Cities.name] = name
}
Users
.batchInsert(users) { user ->
user["name"]?.let { this[Users.name] = it }
user["cityId"]?.let { this[Users.cityId] = it.toInt() }
}
想取出這兩張資料表的內容,並利用 cityId
和 city.id
做一對一的比對,我們可以透過 JOIN 的方式達成
Users
.join(
Cities,
JoinType.INNER,
additionalConstraint = { Users.cityId eq Cities.id })
.selectAll()
.forEach {
println("${it[Users.name]}: ${it[Cities.name]}")
}
這樣我們會看到
Alice: Paris
Bob: Moscow
Carol: Moscow
Dave: Helsinki
Eve: Helsinki
成功的印出了我們使用者的名稱,以及使用者對應城市的名稱。
下面我們來介紹一下上面的操作中,使用到的 kotlin 特殊語法
listOf
在寫入多筆資料時,我們使用了 listOf()
這個函數
val cityNames = listOf("Paris", "Moscow", "Helsinki")
這個函數,根據官方的說明 List,提供一個類似陣列的結構。
由於 kotlin 的特性,所以能夠知道 cityNames
是一個 List<String>
的結構,並且在
Cities
.batchInsert(cityNames) { name ->
this[Cities.name] = name
}
內進行迴圈時,保證了 name
一定是 String
型態,避免程式運行時出現型態衝突的錯誤。
this
我們看到這段程式
Cities
.batchInsert(cityNames) { name ->
this[Cities.name] = name
}
裏面突然用了 this
這個關鍵字。
這是因為在 batchInsert()
時,後方 lambda 實際的輸入值,變成了 cityNames
內部的各個值,也就是這邊所寫的 name
。
所以,要宣告我們寫入的對象,我們變成要呼叫 batchInsert()
參數最後 Lambda 函數內所用到的 BatchInsertStatement
物件。要存取這個物件,用 this
就可以很直觀的取得。
let()
在寫入 Users
資料表時,這邊使用的程式碼如下
val users = listOf(
mapOf("name" to "Alice", "cityId" to "1"),
mapOf("name" to "Bob", "cityId" to "2"),
mapOf("name" to "Carol", "cityId" to "2"),
mapOf("name" to "Dave", "cityId" to "3"),
mapOf("name" to "Eve", "cityId" to "3"),
)
Users
.batchInsert(users) { user ->
user["name"]?.let { this[Users.name] = it }
user["cityId"]?.let { this[Users.cityId] = it.toInt() }
}
mapOf()
的結構比較好推測出來,就是其他語言的 key-value pair 或者 map
結構。但是為什麼這邊要用個 ?.let
來處理後續的行為呢?
這是因為,透過 map
建立出來的 user
資料,結構為 Map<String, String>
,如果我們透過 user["name"]
嘗試取出資料時,這筆資料有可能會是 null
。
在 kotlin 語言中,如果沒有特別宣告可以接受 null
的資料結構,在編譯時就會檢查是否有程式嘗試寫入可能為 null
的值,並拋出編譯錯誤。
也就是說,如果我們不用某種方式,保證我們寫入的值不會是 null
的話,那麼這段程式是無法編譯的。
所以,我們做了一段檢查
user["name"]?.let { this[Users.name] = it }
只有在 user["name"]
不是 null
的時候,才會執行 let()
裏面的內容。
let()
函數的行為,根據 kotlin 官方文件 let,是這樣說的
let
can be used to invoke one or more functions on results of call chains.
這裡我們利用這個函數,讓 this[Users.name] = user["name"]
的操作,必須在確認 user["name"]
不是 null
之後才會執行。
在 let()
後面的 lambda 內,由於呼叫 let()
的對象已經是 user["name"]
,所以我們不用再重複宣告一次這個變數。只要在 let()
裏面宣告 it
,kotlin 就知道我們說的是 user["name"]
了。