昨天我們成功和資料庫進行了串接,但是我們只建立了一張資料表,還沒有實際的處理資料內容。
今天我們嘗試對資料庫進行基本的 CRUD 操作,也就是常說的建立、讀取、修改、刪除四個操作。
在 Exposed 框架內,有兩種操作的方式:DSL 和 DAO。
今天我們先來學習使用 DSL 的方式,和資料庫進行互動。
DSL(Domain Specific Language),簡單的說,就是 Exposed 框架所設計的一系列函數,協助我們不需要直接撰寫 SQL Query 就可以進行對資料庫的存取。
這個做法類似於其他框架或語言的 Query Builder,如果讀者已經有熟悉的資料庫存取框架,可以拿 Exposed 框架的 DSL 和自己熟悉的做法比較看看有什麼異同之處。
調整資料前,我們先將 addLogger(StdOutSqlLogger)
移除,這樣可以簡化我們後面印出的資料內容。
要新增一筆資料,我們在建立資料表後面的程式,加上以下程式碼
Cities.insert {
it[name] = "Taipei"
}
就可以建立資料。
不過單純建立資料,不讀取看看的話,我們也不知道資料是否成功建立。
我們可以用另一種寫法,來取得我們所建立資料的 ID
val id = Cities.insertAndGetId {
it[name] = "Tainan"
}
這樣我們就可以用取得的 id
,從資料庫取出我們之前寫入的資料。
透過 id
取出資料的方式如下
Cities
.select { Cities.id eq id }
.forEach {
println("City #$id: ${it[Cities.name]}")
}
整個 transaction 看起來像是這樣
transaction {
SchemaUtils.create(Cities)
Cities.insert {
it[name] = "Taipei"
}
val id = Cities.insertAndGetId {
it[name] = "Tainan"
}
Cities
.select { Cities.id eq id }
.forEach {
println("City #$id: ${it[Cities.name]}")
}
}
運作程式後,我們應該可以看到
City #2: Tainan
如果要印出所有的 City,我們可以用 selectAll()
這個函數來調整一下程式
transaction {
SchemaUtils.create(Cities)
Cities.insert {
it[name] = "Taipei"
}
val id = Cities.insertAndGetId {
it[name] = "Tainan"
}
Cities
.selectAll()
.forEach {
println("City #${it[Cities.id]}: ${it[Cities.name]}")
}
}
到這邊,我們就可以做到新增和讀取資料了!
要修改資料的話,我們可以這樣做
Cities.update({ Cities.id eq id }) {
it[name] = "Kaohsiung"
}
如果你不希望用 id
找出要修改的資料,也可以使用其他條件
Cities.update({ Cities.name eq "Tainan" }) {
it[name] = "Kaohsiung"
}
加上這段邏輯後,如果我們嘗試印出原本的資料,就會得到
City #2: Kaohsiung
到這邊,我們就可以做到修改資料了!
要刪除資料,我們可以透過 deleteWhere()
函數來移除資料
Cities.deleteWhere { Cities.id eq id }
資料移除之後,無法讀取內容,那我們怎麼知道我們成功刪除了呢?
我們可以透過 count()
函數來取得資料筆數,如果資料筆數為零,我們就知道原先的資料已經不存在,資料刪除成功了
println(
Cities
.select { Cities.id eq id }
.count()
)
成功的話就會印出 0
,證明我們的資料刪除成功。
到這邊,資料的 CRUD 就全部介紹完畢了。
能夠做到資料的 CRUD 之後,下面我們來說明一下前面的程式碼。
it
除了前面介紹過的 Passing trailing lambdas,在一開始,我們最先會注意到的,應該就是寫入資料時
Cities.insert {
it[name] = "Taipei"
}
的這個 it
了。
根據 kotlin 的官方文件Context object: this or it,裏面提到 it
是 Lambda 函數內參數(Argument)的預設值。
也就是說,我們在 insert()
函數裏面指定要操作的資料時,我們沒有特別寫一個變數名稱,來指定我們要寫入的資料內容。
取而代之,我們的做法是透過 Kotlin 提供的預設名稱來撰寫我們需要的內容。直接說 it[name] = "Taipei"
就知道要寫入的資料是什麼樣子了。
類似的邏輯,也出現在要印出所有資料時,我們透過 forEach()
的操作上
Cities
.selectAll()
.forEach {
println("All Cities: ${it[Cities.name]}")
}
只要在 forEach()
內寫 it
,kotlin 就知道指的是我們所取出的單筆資料,並且可以針對單筆資料進行操作。
這樣的設計方式,是不是很直觀呢?
eq
我們會特別注意到的部分,還有取得資料時
Cities
.select { Cities.id eq id }
.forEach {
println("City #$id: ${it[Cities.name]}")
}
所使用的 eq
了,這是什麼做法呢?
在 kotlin 內,除了定義前綴(prefix)與後綴(suffix)之外,還可以特別定義所謂的中綴(infix)函數。這個函數可以從前後接受參數並進行運算,讓我們的語法看起來更加直觀。
在框架的程式碼裡面,是這麼定義 eq
這個函數的
/** Checks if this expression is equals to some [t] value. */
infix fun <T> ExpressionWithColumnType<T>.eq(t: T): Op<Boolean> = if (t == null) isNull() else EqOp(this, wrap(t))
利用 infix
的特性,我們就可以不用類似 eq(Cities.id, id)
的做法,直接寫 Cities.id eq id
,讓語法更加簡潔並且直觀。
"${}"
再來,我們注意到的,應該就是印出資料時
println("City #$id: ${it[Cities.name]}")
所使用的 $id
和 ${it[Cities.name]}
了。
在 kotlin 內,如果我們需要印出某個變數,我們可以透過在印出的字串內加上 $
符號,來加入該變數內容。kotlin 會自動將
這個方式,在 kotlin 內,稱為 string templates
如果我們要印出的內容比較複雜,需要透過表達式(expression)來進行呈現,我們可以用 ${}
包裝整個表達式,就像是範例裏面的 ${it[Cities.name]}
這樣。
到這邊,希望所有的範例都能讓讀者們操作成功並理解了,我們明天見!