Android-Firestore的交易與批次處理

Vincent Zheng
新手工程師的程式教室
7 min readJan 12, 2019

--

在資料庫的領域,「交易」集結了一連串的讀寫動作;而「批次處理」則是只集結寫入動作。它們被稱為原子性操作(atomic operation),當內部所有動作都完成,才算成功,否則會回復為一開始的狀態。

本文介紹交易與批次處理的觀念,並使用程式在Firestore上進行實作。使用的是Kotlin語言。

一、交易的觀念

在Firestore,「交易」是讀寫操作的集合。舉例來說,有一項操作是將學生的請假總天數加1,我們可以想像程式流程會像是:

  1. 讀取學生目前請假紀錄(假設目前累積5天)
  2. 增加請假天數1天
  3. 寫回資料庫(已累積6天)

電腦要完成這些指令,只需要短暫的時間。但若在這一瞬間,有第二個人剛好正在對這個學生做同樣的操作,此時在第一個人尚未寫回的情況下,第二人所讀取到的天數也會是5天。

第一人寫回資料庫後,學生累積6天請假,這是我們預期的。但第二人寫回時,所傳送的資料是他讀取到的天數再加1。這會導致他以6天這個值寫回後,學生依然累積6天!然而兩人先後操作完畢後應該要是7天才對。

上面的例子在資料庫領域稱為「髒讀」,第二人讀到了舊的髒資料。若能使用交易將這3個指令「包裝」起來,這兩人的操作便會「隔離」開來,第一個人完全做完才換下一人,如此就不會讀到髒資料。

二、交易的程式實作

Firestore交易的使用時機為:需要更新某欄位的值,而新的值是基於該文件欄位原本的值(也可以是其他欄位)。就像上一節的情境,新的請假總天數是基於原本的天數再做遞增。

以更新學生的年齡為例,如果要使用交易,程式會這樣子寫:

val studentDoc = db.collection("Students")
.document("RbHrN4klDP...")

db.runTransaction { transaction: Transaction ->
val snapshot: DocumentSnapshot = transaction.get(studentDoc)
var age = snapshot.getLong("age")
age = age!! + 1
transaction.update(studentDoc, "age", age)
null
}

.addOnSuccessListener {
Toast.makeText(applicationContext, "更新成功", Toast.LENGTH_SHORT).show()
}

首先準備學生的文件參照(document reference),這是在交易中要做寫入操作的標的物。接著透過資料庫物件呼叫「runTransaction」方法,並藉由該方法提供的「Transaction」物件繼續完成讀寫操作。

Firestore交易規定,讀取動作必須在寫入動作前進行(否則會拋出例外),因此我們用剛剛的文件參照取得快照(document snapshot)。然後透過快照提供的方法,取得指定欄位的資料,例如getString、getDouble等。

若沒有其他要取得的文件或資料,則可以將欄位值進行處理後寫入,例如set(整筆覆蓋)、delete(刪除),本例使用update來更新部份欄位。如此一來便完成交易的實作了。

這個runTransaction方法有個特性:在交易進行途中,若要讀取的資料剛好被他人修改,這個交易區塊會重新執行,才能確保不會讀到舊的髒資料。

三、交易的callback程式

交易如同Firestore的增刪改查操作,一樣可以配置如「OnSuccessListener」之類的callback程式,然而有些微不同。

在runTransaction方法的最後,可以回傳需要的資料,以便在操作成功時進入OnSuccessListener後取用。我們直接來看範例:

db.runTransaction { transaction: Transaction ->
val snapshot: DocumentSnapshot = transaction.get(studentDoc)
var age = snapshot.getLong("age")
age = age!! + 1
transaction.update(studentDoc, "age", age)
age
}
.addOnSuccessListener { result ->
Toast.makeText(applicationContext, "年齡更新為" + result, Toast.LENGTH_SHORT).show()
}

在交易程式的最後,回傳了age這項資料。等到交易成功,可以在callback程式中用一個result變數去接收它(名稱能自己取)。這個範例便是在交易確實成功後,隨即顯示新的年齡訊息。

不需要回傳值的話,則以null代替即可。但有個重點,最後一行會被視為回傳值。若直接省略,result變數也有可能是個Transaction物件,顯得奇怪。

這裡筆者再度示範,讓交易回傳一個修改過後的學生物件:

db.runTransaction { transaction ->
val snapshot = transaction.get(studentDoc)
val student = snapshot.toObject(Student::class.java)
student?.age = student?.age!! + 1
transaction.update(studentDoc, "age", student.age)

student
}
.addOnSuccessListener { student ->
Toast.makeText(applicationContext, student.name + "的年齡更改為" + student.age, Toast.LENGTH_SHORT).show()
}

四、批次處理

批次處理的觀念與上述的交易比起來,並沒有複雜的細節。簡單來說只要配置好需要的寫入操作,就能透過一次的網路連線,讓Firestore全部執行。

寫入操作包含:set(新增或覆蓋)、update(更新部份欄位)、delete(刪除)。使用批次處理的程式可以像這樣子寫:

val collection = db.collection("Students")
val studentDoc1 = collection.document("9aByWVaBCl29wAZTiRoz")
val studentDoc2 = collection.document("RbHrN4klDP...")
val studentDoc3 = collection.document("Ie47GrO8qH...")

val batch: WriteBatch = db.batch()
batch.set(studentDoc1, student)
batch.update(studentDoc2, "age", 25)
batch.delete(studentDoc3)
batch.commit()
.addOnSuccessListener {
Toast.makeText(applicationContext, "批次處理成功", Toast.LENGTH_SHORT).show()
}

首先要準備寫入操作的標的物,也就是文件參照(document reference)。接著透過資料庫物件取得「WriteBatch」物件,該物件就像「待辦清單」,可以交代要進行的操作給它,但一次最多接受500項寫入操作。

這個範例配置了前面提到的三種寫入操作,它們都必須傳入文件參照作為參數,因為參照就代表了文件的「路徑」。

最後呼叫「commit」方法,提交所有操作進行批次處理即可。這時看看Firestore的網頁,會發現這三項寫入操作一次被執行。

Firestore寫入操作的網頁特效,綠色為新增、紅色為刪除、橘色為更新

--

--

Vincent Zheng
新手工程師的程式教室

我是Vincent,是個來自資管系的後端軟體工程師。當初因為學校作業,才踏出寫部落格的第一步。這裡提供程式教學文章,包含自學和工作上用到的經驗,希望能讓讀者學到東西。我的部落已搬家至 https://chikuwa-tech-study.blogspot.com/