讀取及寫入資料

(選用) 使用 Firebase 模擬器套件設計原型並進行測試

在說明應用程式如何讀取及寫入即時資料庫之前,我們先介紹一組可用於原型設計及測試即時資料庫功能的工具:Firebase 模擬器套件。如果您正在嘗試不同的資料模型、調整安全性規則,或是尋找與後端互動時最經濟實惠的方式,不妨在本地作業,不必部署即時服務。

即時資料庫模擬器是模擬器套件的一部分,可讓應用程式與模擬的資料庫內容和設定互動,以及選擇性地與模擬的專案資源 (函式、其他資料庫和安全性規則) 互動。emulator_suite_short

使用即時資料庫模擬器只需幾個步驟:

  1. 在應用程式的測試設定中加入一行程式碼,即可連線至模擬器。
  2. 從本機專案目錄的根目錄執行 firebase emulators:start
  3. 使用 Realtime Database 平台 SDK,或 Realtime Database REST API,從應用程式的原型程式碼發出呼叫。

我們提供詳細的逐步說明,其中包含即時資料庫和 Cloud Functions。建議您也參閱 模擬器套件簡介

取得 DatabaseReference

如要從資料庫讀取或寫入資料,您需要 DatabaseReference 的執行個體:

DatabaseReference ref = FirebaseDatabase.instance.ref();

寫入資料

本文將介紹讀取及寫入 Firebase 資料的基本概念。

Firebase 資料會寫入 DatabaseReference,並透過等待或監聽參照發出的事件來擷取。系統會針對資料的初始狀態觸發一次事件,並且在資料變更時會再次觸發。

基本寫入作業

如要執行基本寫入作業,可以使用 set() 將資料儲存至指定參照,並取代該路徑中的所有現有資料。您可以將參照設為下列類型:StringbooleanintdoubleMapList

舉例來說,您可以按照下列方式新增使用者:set()

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

await ref.set({
  "name": "John",
  "age": 18,
  "address": {
    "line1": "100 Mountain View"
  }
});

以這種方式使用 set() 會覆寫指定位置的資料,包括所有子節點。不過,您還是可以更新子項,不必重新編寫整個物件。如要允許使用者更新個人資料,可以按照下列步驟更新使用者名稱:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

// Only update the age, leave the name and address!
await ref.update({
  "age": 19,
});

update() 方法會接受節點的子路徑,讓您一次更新資料庫中的多個節點:

DatabaseReference ref = FirebaseDatabase.instance.ref("users");

await ref.update({
  "123/age": 19,
  "123/address/line1": "1 Mountain View",
});

讀取資料

監聽值事件,讀取資料

如要在路徑讀取資料並監聽變更,請使用 DatabaseReferenceonValue 屬性監聽 DatabaseEvent

您可以使用 DatabaseEvent 讀取特定路徑的資料,因為該資料在事件發生時存在。附加監聽器時,系統會觸發一次這個事件,之後每當資料 (包括任何子項) 變更時,系統也會觸發一次。該事件具有 snapshot 屬性,內含該位置的所有資料,包括子項資料。如果沒有資料,快照的 exists 屬性會是 false,而 value 屬性則會是空值。

以下範例說明社交網誌應用程式如何從資料庫擷取貼文詳細資料:

DatabaseReference starCountRef =
        FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
    final data = event.snapshot.value;
    updateStarCount(data);
});

事件發生時,監聽器會在 value 屬性中收到 DataSnapshot,其中包含資料庫中指定位置的資料。

讀取資料一次

使用 get() 讀取一次

無論應用程式處於連線或離線狀態,SDK 都能管理與資料庫伺服器的互動。

一般而言,您應使用上述值事件技術讀取資料,以便在後端資料更新時收到通知。這些技術可減少用量和帳單費用,並經過最佳化,能為使用者提供最佳的線上和離線體驗。

如果只需要資料一次,可以使用 get() 從資料庫取得資料快照。如果 get() 無法傳回伺服器值,用戶端會探查本機儲存空間快取,如果仍找不到該值,就會傳回錯誤。

以下範例說明如何從資料庫擷取使用者的公開使用者名稱:

final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
    print(snapshot.value);
} else {
    print('No data available.');
}

不必要地使用 get() 會增加頻寬用量,導致效能降低。如要避免這種情況,請使用上述即時監聽器。

使用 once() 讀取資料一次

在某些情況下,您可能希望系統立即傳回本機快取中的值,而不是檢查伺服器上是否有更新的值。在這種情況下,您可以使用 once() 立即從本機磁碟快取取得資料。

這類資料只需要載入一次,且預期不會經常變更或需要主動監聽,因此非常實用。舉例來說,在先前的範例中,網誌應用程式會在使用者開始撰寫新文章時,使用這個方法載入使用者設定檔:

final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';

更新或刪除資料

更新特定欄位

如要同時寫入節點的特定子項,而不覆寫其他子項節點,請使用 update() 方法。

呼叫 update() 時,您可以指定鍵的路徑,更新下層子項的值。如果資料儲存在多個位置,以便更妥善地擴充,您可以使用資料擴散傳遞功能更新所有資料例項。舉例來說,社群部落格應用程式可能想建立貼文,並同時更新至最近活動動態消息和發布貼文的使用者活動動態消息。為此,網誌應用程式會使用類似下列的程式碼:

void writeNewPost(String uid, String username, String picture, String title,
        String body) async {
    // A post entry.
    final postData = {
        'author': username,
        'uid': uid,
        'body': body,
        'title': title,
        'starCount': 0,
        'authorPic': picture,
    };

    // Get a key for a new Post.
    final newPostKey =
        FirebaseDatabase.instance.ref().child('posts').push().key;

    // Write the new post's data simultaneously in the posts list and the
    // user's post list.
    final Map<String, Map> updates = {};
    updates['/posts/$newPostKey'] = postData;
    updates['/user-posts/$uid/$newPostKey'] = postData;

    return FirebaseDatabase.instance.ref().update(updates);
}

這個範例會使用 push() 在節點中建立貼文,其中包含 /posts/$postid 中所有使用者的貼文,並同時使用 key 擷取金鑰。然後,您可以使用這個鍵在 /user-posts/$userid/$postid 的使用者貼文中建立第二個項目。

使用這些路徑,您只需呼叫一次 update(),即可同時更新 JSON 樹狀結構中的多個位置,例如這個範例會在兩個位置建立新貼文。以這種方式進行的同步更新是不可分割的作業:所有更新都會成功,或所有更新都會失敗。

新增完成回呼

如要瞭解資料的提交時間,可以註冊完成回呼。set()update() 都會傳回 Future,您可以將成功和錯誤回呼附加至該物件,以便在寫入作業已提交至資料庫,以及呼叫失敗時呼叫這些回呼。

FirebaseDatabase.instance
    .ref('users/$userId/email')
    .set(emailAddress)
    .then((_) {
        // Data saved successfully!
    })
    .catchError((error) {
        // The write failed...
    });

刪除資料

如要刪除資料,最簡單的方法是在該資料位置的參照上呼叫 remove()

您也可以指定空值做為其他寫入作業 (例如 set()update()) 的值,藉此刪除資料。您可以使用這項技巧搭配 update(),在單一 API 呼叫中刪除多個子項。

將資料儲存為交易

處理可能因並行修改而損毀的資料 (例如遞增計數器) 時,您可以將交易處理常式傳遞至 runTransaction(),藉此使用交易。交易處理常式會將資料的目前狀態做為引數,並傳回您想寫入的新所需狀態。如果其他用戶端在您的新值成功寫入位置之前寫入該位置,系統會使用新的目前值再次呼叫更新函式,並重試寫入作業。

舉例來說,在範例社群網誌應用程式中,您可以允許使用者為貼文加上或移除星號,並追蹤貼文獲得的星號數量,如下所示:

void toggleStar(String uid) async {
  DatabaseReference postRef =
      FirebaseDatabase.instance.ref("posts/foo-bar-123");

  TransactionResult result = await postRef.runTransaction((Object? post) {
    // Ensure a post at the ref exists.
    if (post == null) {
      return Transaction.abort();
    }

    Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
    if (_post["stars"] is Map && _post["stars"][uid] != null) {
      _post["starCount"] = (_post["starCount"] ?? 1) - 1;
      _post["stars"][uid] = null;
    } else {
      _post["starCount"] = (_post["starCount"] ?? 0) + 1;
      if (!_post.containsKey("stars")) {
        _post["stars"] = {};
      }
      _post["stars"][uid] = true;
    }

    // Return the new data.
    return Transaction.success(_post);
  });
}

根據預設,每次執行交易更新函式時都會引發事件,因此如果多次執行函式,可能會看到中間狀態。您可以將 applyLocally 設為 false,禁止顯示這些中間狀態,並等待交易完成後再引發事件:

await ref.runTransaction((Object? post) {
  // ...
}, applyLocally: false);

交易結果為 TransactionResult,其中包含交易是否已提交,以及新快照等資訊:

DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");

TransactionResult result = await ref.runTransaction((Object? post) {
  // ...
});

print('Committed? ${result.committed}'); // true / false
print('Snapshot? ${result.snapshot}'); // DataSnapshot

取消交易

如要安全地取消交易,請呼叫 Transaction.abort() 來擲回 AbortTransactionException

TransactionResult result = await ref.runTransaction((Object? user) {
  if (user !== null) {
    return Transaction.abort();
  }

  // ...
});

print(result.committed); // false

不可分割的伺服器端遞增

在上述用途中,我們將兩個值寫入資料庫:為貼文加上/取消加星號的使用者 ID,以及遞增的星號數。如果我們已知道使用者正在為貼文加上星號,則可使用原子遞增作業,而非交易。

void addStar(uid, key) async {
  Map<String, Object?> updates = {};
  updates["posts/$key/stars/$uid"] = true;
  updates["posts/$key/starCount"] = ServerValue.increment(1);
  updates["user-posts/$key/stars/$uid"] = true;
  updates["user-posts/$key/starCount"] = ServerValue.increment(1);
  return FirebaseDatabase.instance.ref().update(updates);
}

這段程式碼不會使用交易作業,因此如果發生更新衝突,系統不會自動重新執行。不過,由於遞增作業是直接在資料庫伺服器上進行,因此不會發生衝突。

如要偵測並拒絕應用程式專屬的衝突,例如使用者為已加星號的貼文加上星號,請為該用途編寫自訂安全規則。

離線處理資料

如果用戶端失去網路連線,應用程式仍會繼續正常運作。

連線至 Firebase 資料庫的每個用戶端都會維護任何有效資料的內部版本。寫入資料時,系統會先寫入這個本機版本。Firebase 用戶端隨後會盡力將資料與遠端資料庫伺服器和其他用戶端同步處理。

因此,所有寫入資料庫的作業都會立即觸發本機事件,然後才將資料寫入伺服器。也就是說,無論網路延遲或連線狀況如何,應用程式都能保持回應。

重新建立連線後,應用程式會收到適當的事件集,讓用戶端與目前的伺服器狀態同步,不必撰寫任何自訂程式碼。

如要進一步瞭解離線行為,請參閱「進一步瞭解線上和離線功能」。

後續步驟