读取和写入数据 (Web)

(可选)使用 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);
});

使用观测器 (observer) 读取数据一次

在某些情况下,您可能希望立即返回本地缓存中的值,而不必检查服务器上更新后的值。在这些情况下,您可以使用 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() 时,您可以通过为键指定路径来更新较低层级的子节点值。如果为了更好地实现伸缩而将数据存储在多个位置,可使用数据扇出更新这些数据的所有实例。

例如,某个社交博客应用可能会创建一篇博文,并将其同时更新到近期活动 Feed 和发布用户活动的 Feed,使用的代码如下所示:

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 客户端会尽可能将这些数据与远程数据库服务器以及其他客户端同步。

因此,对数据库执行的所有写入操作会立即触发本地事件,然后数据才会写入服务器。这意味着应用仍将保持随时响应的状态,无论网络延迟或连接状况如何。

连接重新建立之后,您的应用将收到一系列相应的事件,以便客户端与当前服务器状态进行同步,而不必编写任何自定义代码。

我们将在详细了解在线和离线功能中详细介绍离线行为。

后续步骤