Catch up on everything announced at Firebase Summit, and learn how Firebase can help you accelerate app development and run your app with confidence. Learn More

读取和写入数据

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

(可选)使用 Firebase Emulator Suite 进行原型设计和测试

在讨论您的应用如何读取和写入实时数据库之前,让我们介绍一组可用于原型和测试实时数据库功能的工具:Firebase Emulator Suite。如果您正在尝试不同的数据模型,优化您的安全规则,或努力寻找与后端交互的最具成本效益的方式,那么无需部署实时服务即可在本地工作可能是一个好主意。

Realtime Database 模拟器是 Emulator Suite 的一部分,它使您的应用程序能够与您的模拟数据库内容和配置以及可选的模拟项目资源(函数、其他数据库和安全规则)进行交互。emulator_suite_short

使用实时数据库模拟器只需几个步骤:

  1. 在应用程序的测试配置中添加一行代码以连接到模拟器。
  2. 从本地项目目录的根目录运行firebase emulators:start
  3. 像往常一样使用实时数据库平台 SDK 或使用实时数据库 REST API 从应用程序的原型代码进行调用。

提供了涉及实时数据库和云函数的详细演练。您还应该看看Emulator Suite 介绍

获取数据库引用

要从数据库读取或写入数据,您需要一个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 name, leave the age 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属性为 null。

以下示例演示了一个社交博客应用程序从数据库中检索帖子的详细信息:

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()时,您可以通过指定键的路径来更新较低级别的子值。如果数据存储在多个位置以更好地扩展,您可以使用data fan-out更新该数据的所有实例。例如,社交博客应用程序可能想要创建帖子并同时将其更新为最近的活动提要和发布用户的活动提要。为此,博客应用程序使用如下代码:

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 检索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()

您还可以通过将 null 指定为另一个写入操作的值来删除,例如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 客户端会在“尽力而为”的基础上将该数据与远程数据库服务器和其他客户端同步。

因此,在任何数据写入服务器之前,对数据库的所有写入都会立即触发本地事件。这意味着无论网络延迟或连接如何,您的应用都会保持响应。

重新建立连接后,您的应用程序会收到一组适当的事件,以便客户端与当前服务器状态同步,而无需编写任何自定义代码。

我们将在了解有关在线和离线功能的更多信息中详细讨论离线行为。

下一步