利用 Cloud Firestore 實現資料的建立,讀取,修改 & 刪除

Firebase 提供強大的 Firestore 讓我們上傳和下載資料,接下來讓我們一步步認識它,利用它實現資料的建立,讀取,修改 & 刪除。

使用 FirebaseFirestoreSwift 前的準備動作

建立 Firebase 專案

設定 iOS App 的 Firebase 功能和安裝 FirebaseFirestoreSwift 套件

設定 Firebase 專案的 Cloud Firestore 功能

Cloud Firestore Data Model 介紹

從 Firebase console 手動輸入 Cloud Firestore 的 database 內容

Cloud Firestore 的資料如下,collection songs 裡儲存夜夜伴彼得潘入眠的好歌。若想資料都從程式上傳,也可忽略此步驟。

import FirebaseFirestore & FirebaseFirestoreSwift

import FirebaseFirestore
import FirebaseFirestoreSwift

import FirebaseFirestore 才能從 Firestore 存取資料,import FirebaseFirestoreSwift 才能將資料對應到我們自訂的型別。

定義 Firestore 資料對應的自訂型別

  • 自訂 Firestore 資料對應的 Codable 型別

我們希望從 Cloud Firestore 抓取的資料變成以下的自訂型別 Song。為了讓資料有識別的 id & 遵從 protocol Identifiable,我們另外加入 @DocumentID var id: String?,到時候抓到資料時,id 的內容將為資料的 document id。

import FirebaseFirestoreSwift

struct Song: Codable, Identifiable {
@DocumentID var id: String?
let name: String
let singer: String
let rate: Int
}

新增資料(上傳 Document)

以下分別介紹自動產生文件 id 和指定文件 id 的寫法。

自動產生文件 id

呼叫 function addDocument 上傳資料。

func createSong() {
let db = Firestore.firestore()

let song = Song(name: "陪你很久很久", singer: "小球", rate: 5)
do {
let documentReference = try db.collection("songs").addDocument(from: song)
print(documentReference.documentID)
} catch {
print(error)
}
}

呼叫 function addDocument(from:) 上傳資料,它的參數型別為 Encodable,因此我們可傳入 Song 型別的資料。

上傳成功後,可在 Firebase 網站上專案的 Cloud Firestore 頁面看到資料。

指定文件 id 的寫法

透過文件 id 陪你很久很久取得 DocumentReference ,然後呼叫 function setData 設定資料。

func createSong() {
let db = Firestore.firestore()

let song = Song(name: "陪你很久很久", singer: "小球", rate: 5)

do {
try db.collection("songs").document("陪你很久很久").setData(from: song)
} catch {
print(error)
}
}

利用 FirestoreQuery 讀取 Firebase Firestore 資料庫

使用 SwiftUI 的朋友可利用 FirestoreQuery 讀取 Firebase Firestore 資料庫。

讀取某個 collection 下全部的 documents

呼叫 function getDocuments 抓取資料。

func fetchSongs() {
let db = Firestore.firestore()
db.collection("songs").getDocuments { snapshot, error in

guard let snapshot else { return }

let songs = snapshot.documents.compactMap { snapshot in
try? snapshot.data(as: Song.self)
}
print(songs)

}
}

從 Firestore 抓取的資料是型別 [QueryDocumentSnapshot] 的 documents,我們想將它轉成型別 [Song],因此我們從 snapshot.documents 呼叫 function compactMap,在 compactMap 的 closure 裡生成型別 Song 的資料。

let songs = snapshot.documents.compactMap { snapshot in
try? snapshot.data(as: Song.self)
}

參數 snapshot 的型別為 QueryDocumentSnapshot,呼叫它的 function data(as:) 可將資料變成自訂型別。在此我們傳入 Song.self得到型別 Song 的資料。

如下圖所示,compactMap 回傳的資料型別為 [Song]。

ps: 我們也可以從 snapshot.data() 讀取 document 的內容,它的型別為 [String : Any]。

func fetchSongs() {
let db = Firestore.firestore()
db.collection("songs").getDocuments { snapshot, error in

guard let snapshot else { return }

snapshot.documents.forEach { snapshot in
print(snapshot.data()["name"] ?? "")
}
}
}

排序

以 order(by:) 排序,由小到大

func fetchSongs() {
let db = Firestore.firestore()
db.collection("songs").order(by: "rate").getDocuments { snapshot, error in
guard let snapshot else { return }

let songs = snapshot.documents.compactMap { snapshot in
try? snapshot.data(as: Song.self)
}
print(songs)
}
}

以多個欄位排序,比方先比身高,身高一樣再比體重

let db = Firestore.firestore()
db.collection("students").order(by: "height").order(by: "weight").getDocuments { snapshot, error in

}

以 order 的參數 descending 為 true 控制排序由大到小

let db = Firestore.firestore()
db.collection("songs").order(by: "rate", descending: true).getDocuments { snapshot, error in

}

設定數量

以 limit(to:) 指定抓取的數量。

let db = Firestore.firestore()
db.collection("songs").order(by: "rate").limit(to: 2).getDocuments { snapshot, error in

}

搜尋特定的資料

利用 whereField 設定 search 的條件。

let db = Firestore.firestore()
db.collection("songs").whereField("singer", isEqualTo: "周興哲").getDocuments { snapshot, error in

}

whereField 有很多設定條件的方法,有興趣的朋友可進一步查詢相關資訊。

檢查特定條件的資料是否存在。

let db = Firestore.firestore()
db.collection("songs").whereField("singer", isEqualTo: "周興哲").getDocuments { snapshot, error in

guard let snapshot else { return }
if snapshot.documents.isEmpty {

} else {

}
}

Firebase Firestore 多個欄位 query 的 index 問題

抓取資料時若有用到多個欄位,將遇到資料無法抓取的問題。比方以下程式的 query 會判斷 mood & rate,抓取以 rate 排序的悲傷情歌。

let db = Firestore.firestore()
db.collection("songs").whereField("mood", isEqualTo: "😢").order(by: "rate").getDocuments { snapshot, error in

}

此問題可透過加上 index 解決,相關說明可參考以下連結。

透過文件 id 讀取某個 document

透過文件 id 陪你很久很久取得 DocumentReference ,然後呼叫 function getDocument。

func getFavoriteSong() {
let db = Firestore.firestore()
db.collection("songs").document("陪你很久很久").getDocument { document, error in

guard let document,
document.exists,
let song = try? document.data(as: Song.self) else {
return
}
print(song)

}
}

讀取某個 document 下的 collection 的 documents

假設 Firestore 的資料如下。(註: document 下可以有 collection,collection 下有 document,因此可以實現非常多層的架構。)

let db = Firestore.firestore()
db.collection("music").document("周杰倫").collection("albums").getDocuments { querySnapshot, error in

}

利用路徑讀取

假設 Firestore 的資料如下。

let db = Firestore.firestore()
db.collection("music/周杰倫/albums").getDocuments { querySnapshot, error in

}

持續偵測資料是否有更新

持續偵測 collection 下的 document 是否有新增,刪除,修改

一開始 addSnapshotListener 會先抓到 collection 下所有的 document, 因此 documentChanges 將包含所有的 document,type 則為 .added。之後 addSnapshotListener 的 closure 再被觸發時,documentChanges 將只包含新增,刪除(removed)或修改(modified)的 document。

func checkSongsChange() {
let db = Firestore.firestore()
db.collection("songs").addSnapshotListener { snapshot, error in
guard let snapshot else { return }
snapshot.documentChanges.forEach { documentChange in
guard let song = try? documentChange.document.data(as: Song.self) else { return }
switch documentChange.type {
case .added:
print("added", song)
case .modified:
print("modified", song)
case .removed:
print("removed", song)
}
}
}
}

若只想偵測某些 document 的變化,可搭配 whereField 設定條件。

let db = Firestore.firestore()

db.collection("songs").whereField("singer", isEqualTo: "小球").addSnapshotListener { snapshot, error in

}

持續偵測 document 是否有更新

func checkSongChange() {
let db = Firestore.firestore()
db.collection("songs").document("陪你很久很久").addSnapshotListener { snapshot, error in
guard let snapshot else { return }
guard let song = try? snapshot.data(as: Song.self) else { return }

print(song)
}
}

修改資料(更新 Document)

將 Song 的 singer & rate 宣告為可修改的變數。

struct Song: Codable, Identifiable {
@DocumentID var id: String?
let name: String
var singer: String
var rate: Int
}

範例 1: 已知文件 id

修改陪你很久很久的歌手跟評分。

將歌手改成大球,評分改成 5。

func modifySong() {
let db = Firestore.firestore()
let documentReference =
db.collection("songs").document("陪你很久很久")
documentReference.getDocument { document, error in

guard let document,
document.exists,
var song = try? document.data(as: Song.self)
else {
return
}
song.rate = 5
song.singer = "大球"
do {
try documentReference.setData(from: song)
} catch {
print(error)
}

}
}

範例 2: 先讀取文件,有了文件 id 後再修改文件

修改抓到的第一首歌。

func modifyFirstSong() {
let db = Firestore.firestore()
db.collection("songs").getDocuments { snapshot, error in

guard let snapshot else { return }

let songs = snapshot.documents.compactMap { snapshot in
try? snapshot.data(as: Song.self)
}
guard var firstSong = songs.first else { return }
firstSong.rate = 2
do {
try db.collection("songs").document(firstSong.id ?? "").setData(from: firstSong)
} catch {
print(error)
}

}
}

刪除資料(delete Document)

let db = Firestore.firestore()
let documentReference = db.collection("songs").document("陪你很久很久")
documentReference.delete()

分頁功能

--

--

彼得潘的 iOS App Neverland
彼得潘的 Swift iOS App 開發問題解答集

彼得潘的iOS App程式設計入門,文組生的iOS App程式設計入門講師,彼得潘的 Swift 程式設計入門,App程式設計入門作者,http://apppeterpan.strikingly.com