在網路上讀取及寫入資料

(選用) 使用 Firebase Local Emulator Suite 設計原型並進行測試

在討論應用程式如何讀取及寫入 Realtime Database 之前,讓我們先介紹一組可用於製作原型和測試 Realtime Database 功能的工具:Firebase Local Emulator Suite。如果您想嘗試不同的資料模型、最佳化安全性規則,或是尋找與後端互動最具成本效益的方式,不妨考慮在本機作業,而非部署實際服務。

Realtime Database 模擬器是 Local Emulator Suite 的一部分,可讓應用程式與模擬資料庫內容和設定互動,以及視需要與模擬的專案資源 (函式、其他資料庫和安全性規則) 互動。

使用 Realtime Database 模擬器只需幾個步驟:

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

您可以參考這篇文章,瞭解如何使用 Realtime DatabaseCloud Functions。您也應該參閱 Local Emulator Suite 簡介

取得資料庫參照

如要從資料庫讀取或寫入資料,您需要 firebase.database.Reference 的例項:

Web

import { getDatabase } from "firebase/database";

const database = getDatabase();

Web

var database = firebase.database();

寫入資料

本文將說明擷取資料的基本概念,以及如何排序及篩選 Firebase 資料。

將非同步事件監聽器附加至 firebase.database.Reference,即可擷取 Firebase 資料。監聽器會針對資料的初始狀態觸發一次,並且在資料變更時會再次觸發。

基本寫入作業

對於基本寫入作業,您可以使用 set() 將資料儲存至指定的參照,取代該路徑上的任何現有資料。舉例來說,社交部落格應用程式可能會使用 set() 新增使用者,如下所示:

Web

import { getDatabase, ref, set } from "firebase/database";

function writeUserData(userId, name, email, imageUrl) {
  const db = getDatabase();
  set(ref(db, 'users/' + userId), {
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

Web

function writeUserData(userId, name, email, imageUrl) {
  firebase.database().ref('users/' + userId).set({
    username: name,
    email: email,
    profile_picture : imageUrl
  });
}

使用 set() 會覆寫指定位置的資料,包括任何子節點。

讀取資料

監聽值事件

如要讀取路徑中的資料並監聽變更,請使用 onValue() 觀察事件。您可以使用這個事件,讀取指定路徑中內容的靜態快照,因為這些內容會在事件發生時存在。這個方法會在連結監聽器時觸發一次,並且在資料 (包括子項) 每次變更時再次觸發。事件回呼會傳遞快照,其中包含該位置的所有資料,包括子資料。如果沒有資料,快照會在您呼叫 exists() 時傳回 false,在您對其呼叫 val() 時傳回 null

以下範例示範社交網站的網誌應用程式如何從資料庫擷取文章的星號數量:

Web

import { getDatabase, ref, onValue } from "firebase/database";

const db = getDatabase();
const starCountRef = ref(db, 'posts/' + postId + '/starCount');
onValue(starCountRef, (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

Web

var starCountRef = firebase.database().ref('posts/' + postId + '/starCount');
starCountRef.on('value', (snapshot) => {
  const data = snapshot.val();
  updateStarCount(postElement, data);
});

事件監聽器會收到 snapshot,其中包含事件發生時資料庫中指定位置的資料。您可以使用 val() 方法擷取 snapshot 中的資料。

讀取資料一次

使用 get() 讀取資料一次

無論應用程式是線上還是離線,SDK 都能管理與資料庫伺服器的互動。

一般來說,您應該使用上述的值事件技巧來讀取資料,以便收到來自後端的資料更新通知。事件監聽器技巧可減少使用量和帳單費用,並經過最佳化處理,讓使用者在線上和離線時都能享有最佳體驗。

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

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

Web

import { getDatabase, ref, child, get } from "firebase/database";

const dbRef = ref(getDatabase());
get(child(dbRef, `users/${userId}`)).then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

Web

const dbRef = firebase.database().ref();
dbRef.child("users").child(userId).get().then((snapshot) => {
  if (snapshot.exists()) {
    console.log(snapshot.val());
  } else {
    console.log("No data available");
  }
}).catch((error) => {
  console.error(error);
});

使用觀察器讀取資料一次

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

這類資料只需載入一次,且不會經常變更或需要主動聆聽,因此非常實用。舉例來說,上例中的部落格應用程式會在使用者開始撰寫新文章時,使用這個方法載入使用者個人資料:

Web

import { getDatabase, ref, onValue } from "firebase/database";
import { getAuth } from "firebase/auth";

const db = getDatabase();
const auth = getAuth();

const userId = auth.currentUser.uid;
return onValue(ref(db, '/users/' + userId), (snapshot) => {
  const username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
}, {
  onlyOnce: true
});

Web

var userId = firebase.auth().currentUser.uid;
return firebase.database().ref('/users/' + userId).once('value').then((snapshot) => {
  var username = (snapshot.val() && snapshot.val().username) || 'Anonymous';
  // ...
});

更新或刪除資料

更新特定欄位

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

呼叫 update() 時,您可以指定索引鍵的路徑,藉此更新較低層級的子項值。如果資料儲存在多個位置,可讓資料更容易擴充,您可以使用資料分支更新該資料的所有例項。

舉例來說,社群網站的部落格應用程式可能會建立一則貼文,並同時使用以下程式碼將其更新至最近活動動態消息,以及發布貼文的使用者動態消息:

Web

import { getDatabase, ref, child, push, update } from "firebase/database";

function writeNewPost(uid, username, picture, title, body) {
  const db = getDatabase();

  // A post entry.
  const postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  const newPostKey = push(child(ref(db), 'posts')).key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  const updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return update(ref(db), updates);
}

Web

function writeNewPost(uid, username, picture, title, body) {
  // A post entry.
  var postData = {
    author: username,
    uid: uid,
    body: body,
    title: title,
    starCount: 0,
    authorPic: picture
  };

  // Get a key for a new Post.
  var newPostKey = firebase.database().ref().child('posts').push().key;

  // Write the new post's data simultaneously in the posts list and the user's post list.
  var updates = {};
  updates['/posts/' + newPostKey] = postData;
  updates['/user-posts/' + uid + '/' + newPostKey] = postData;

  return firebase.database().ref().update(updates);
}

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

透過這些路徑,您可以透過一次呼叫 update() 來同時更新 JSON 樹狀結構中的多個位置,例如這個範例如何在兩個位置建立新貼文。以這種方式進行的同時更新作業是原子性的:所有更新都會成功,或所有更新都會失敗。

新增完成回呼

如要瞭解資料何時已提交,您可以新增完成回呼。set()update() 都會採用選用的完成回呼,在寫入作業已提交至資料庫時呼叫。如果呼叫失敗,回呼會傳遞錯誤物件,指出發生失敗的原因。

Web

import { getDatabase, ref, set } from "firebase/database";

const db = getDatabase();
set(ref(db, 'users/' + userId), {
  username: name,
  email: email,
  profile_picture : imageUrl
})
.then(() => {
  // Data saved successfully!
})
.catch((error) => {
  // The write failed...
});

Web

firebase.database().ref('users/' + userId).set({
  username: name,
  email: email,
  profile_picture : imageUrl
}, (error) => {
  if (error) {
    // The write failed...
  } else {
    // Data saved successfully!
  }
});

刪除資料

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

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

接收 Promise

如要瞭解資料何時提交至 Firebase Realtime Database 伺服器,您可以使用 Promiseset()update() 都能傳回 Promise,您可以利用這項資訊瞭解寫入作業何時提交至資料庫。

卸離監聽器

您可以呼叫 Firebase 資料庫參考資料的 off() 方法,移除回呼。

您可以將單一事件監聽器做為參數傳遞至 off(),藉此移除該事件監聽器。在位置上呼叫 off() 時不帶任何引數,會移除該位置的所有事件監聽器。

在父項事件監聽器上呼叫 off() 不會自動移除在子項節點上註冊的事件監聽器;您必須在任何子項事件監聽器上呼叫 off(),才能移除回呼。

將資料儲存為交易

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

舉例來說,在上述社群網誌應用程式中,您可以允許使用者為貼文按讚或取消按讚,並追蹤貼文獲得的按讚次數,如下所示:

Web

import { getDatabase, ref, runTransaction } from "firebase/database";

function toggleStar(uid) {
  const db = getDatabase();
  const postRef = ref(db, '/posts/foo-bar-123');

  runTransaction(postRef, (post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

Web

function toggleStar(postRef, uid) {
  postRef.transaction((post) => {
    if (post) {
      if (post.stars && post.stars[uid]) {
        post.starCount--;
        post.stars[uid] = null;
      } else {
        post.starCount++;
        if (!post.stars) {
          post.stars = {};
        }
        post.stars[uid] = true;
      }
    }
    return post;
  });
}

如果有多位使用者同時為同一個貼文按下星號,或是用戶端有過時的資料,使用交易可避免星號計數出現錯誤。如果交易遭到拒絕,伺服器會將目前的值傳回至用戶端,後者會使用更新後的值再次執行交易。這項動作會重複執行,直到交易獲得接受或您中止交易為止。

原子伺服器端遞增

在上述用途中,我們會將兩個值寫入資料庫:為貼文加上/移除星號的使用者 ID,以及增加的星號數量。如果我們已知使用者將該貼文設為收藏,就可以使用原子遞增作業,而非交易。

Web

function addStar(uid, key) {
  import { getDatabase, increment, ref, update } from "firebase/database";
  const dbRef = ref(getDatabase());

  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = increment(1);
  update(dbRef, updates);
}

Web

function addStar(uid, key) {
  const updates = {};
  updates[`posts/${key}/stars/${uid}`] = true;
  updates[`posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  updates[`user-posts/${key}/stars/${uid}`] = true;
  updates[`user-posts/${key}/starCount`] = firebase.database.ServerValue.increment(1);
  firebase.database().ref().update(updates);
}

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

如果您想偵測及拒絕應用程式專屬的衝突,例如使用者為自己先前已收藏的貼文收藏,請為該用途撰寫自訂安全性規則。

離線處理資料

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

每個連線至 Firebase 資料庫的用戶端都會維護任何有效資料的內部版本。資料寫入時,系統會先將資料寫入這個本機版本。接著,Firebase 用戶端會以「盡力而為」的方式,將這些資料與遠端資料庫伺服器和其他用戶端同步。

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

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

我們會在「進一步瞭解線上和離線功能」一文中進一步討論離線行為。

後續步驟