获取我们在 Firebase 峰会上发布的所有信息,了解 Firebase 可如何帮助您加快应用开发速度并满怀信心地运行应用。了解详情

在 Apple 平台上读取和写入数据

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

(可选)使用 Firebase 本地模拟器套件进行原型设计和测试

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

实时数据库模拟器是本地模拟器套件的一部分,它使您的应用程序能够与您的模拟数据库内容和配置以及您的模拟项目资源(函数、其他数据库和安全规则)进行交互。

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

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

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

获取 FIRDatabaseReference

要从数据库读取或写入数据,您需要一个FIRDatabaseReference实例:

迅速

注意:此 Firebase 产品不适用于 App Clip 目标。
var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

注意:此 Firebase 产品不适用于 App Clip 目标。
@property (strong, nonatomic) FIRDatabaseReference *ref;

self.ref = [[FIRDatabase database] reference];

写入数据

本文档涵盖了读取和写入 Firebase 数据的基础知识。

Firebase 数据被写入Database引用并通过将异步侦听器附加到引用来检索。侦听器会针对数据的初始状态触发一次,并在数据更改时再次触发。

基本写操作

对于基本的写入操作,您可以使用setValue将数据保存到指定的引用,替换该路径中的任何现有数据。您可以使用此方法:

  • 传递与可用 JSON 类型相对应的类型,如下所示:
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

例如,您可以使用setValue添加用户,如下所示:

迅速

注意:此 Firebase 产品不适用于 App Clip 目标。
self.ref.child("users").child(user.uid).setValue(["username": username])

Objective-C

注意:此 Firebase 产品不适用于 App Clip 目标。
[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

以这种方式使用setValue会覆盖指定位置的数据,包括任何子节点。但是,您仍然可以在不重写整个对象的情况下更新子对象。如果您想允许用户更新他们的个人资料,您可以按如下方式更新用户名:

迅速

注意:此 Firebase 产品不适用于 App Clip 目标。
self.ref.child("users/\(user.uid)/username").setValue(username)

Objective-C

注意:此 Firebase 产品不适用于 App Clip 目标。
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

读取数据

通过监听值事件读取数据

要读取路径上的数据并侦听更改,请使用 FIRDatabaseReference 的observeEventType:withBlock FIRDatabaseReference来观察FIRDataEventTypeValue事件。

事件类型典型用法
FIRDataEventTypeValue阅读并监听路径全部内容的变化。

您可以使用FIRDataEventTypeValue事件在给定路径读取数据,因为它在事件发生时存在。此方法在附加侦听器时触发一次,并且在每次数据(包括任何子项)发生更改时再次触发。事件回调被传递一个snapshot ,其中包含该位置的所有数据,包括子数据。如果没有数据,当你调用exists()时快照将返回false ,当你读取它的value属性时返回nil

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

迅速

注意:此 Firebase 产品不适用于 App Clip 目标。
refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Objective-C

注意:此 Firebase 产品不适用于 App Clip 目标。
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

侦听器在其value属性中接收到FIRDataSnapshot ,其中包含事件发生时数据库中指定位置的数据。您可以将值分配给适当的本机类型,例如NSDictionary 。如果该位置不存在数据,则value nil

读取数据一次

使用 getData() 读取一次

SDK 旨在管理与数据库服务器的交互,无论您的应用程序是在线还是离线。

通常,您应该使用上述值事件技术来读取数据,以便从后端获取数据更新的通知。这些技术可减少您的使用量和计费,并经过优化以在您的用户在线和离线时为他们提供最佳体验。

如果您只需要一次数据,您可以使用getData()从数据库中获取数据的快照。如果由于任何原因getData()无法返回服务器值,客户端将探测本地存储缓存,如果仍未找到该值,则返回错误。

以下示例演示了从数据库中一次检索用户面向公众的用户名:

迅速

注意:此 Firebase 产品不适用于 App Clip 目标。
ref.child("users/\(uid)/username").getData(completion:  { error, snapshot in
  guard error == nil else {
    print(error!.localizedDescription)
    return;
  }
  let userName = snapshot.value as? String ?? "Unknown";
});

Objective-C

注意:此 Firebase 产品不适用于 App Clip 目标。
NSString *userPath = [NSString stringWithFormat:@"users/%@/username", uid];
[[ref child:userPath] getDataWithCompletionBlock:^(NSError * _Nullable error, FIRDataSnapshot * _Nonnull snapshot) {
  if (error) {
    NSLog(@"Received an error %@", error);
    return;
  }
  NSString *userName = snapshot.value;
}];

不必要地使用getData()会增加带宽的使用并导致性能损失,这可以通过使用实时侦听器来防止,如上所示。

与观察者一起读取数据一次

在某些情况下,您可能希望立即返回本地缓存中的值,而不是检查服务器上的更新值。在这些情况下,您可以使用observeSingleEventOfType立即从本地磁盘缓存中获取数据。

这对于只需要加载一次并且预计不会频繁更改或需要主动侦听的数据很有用。例如,前面示例中的博客应用程序在用户开始撰写新帖子时使用此方法加载用户的个人资料:

迅速

注意:此 Firebase 产品不适用于 App Clip 目标。
let userID = Auth.auth().currentUser?.uid
ref.child("users").child(userID!).observeSingleEvent(of: .value, with: { snapshot in
  // Get user value
  let value = snapshot.value as? NSDictionary
  let username = value?["username"] as? String ?? ""
  let user = User(username: username)

  // ...
}) { error in
  print(error.localizedDescription)
}

Objective-C

注意:此 Firebase 产品不适用于 App Clip 目标。
NSString *userID = [FIRAuth auth].currentUser.uid;
[[[_ref child:@"users"] child:userID] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  // Get user value
  User *user = [[User alloc] initWithUsername:snapshot.value[@"username"]];

  // ...
} withCancelBlock:^(NSError * _Nonnull error) {
  NSLog(@"%@", error.localizedDescription);
}];

更新或删除数据

更新特定字段

要同时写入一个节点的特定子节点而不覆盖其他子节点,请使用updateChildValues方法。

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

迅速

注意:此 Firebase 产品不适用于 App Clip 目标。
guard let key = ref.child("posts").childByAutoId().key else { return }
let post = ["uid": userID,
            "author": username,
            "title": title,
            "body": body]
let childUpdates = ["/posts/\(key)": post,
                    "/user-posts/\(userID)/\(key)/": post]
ref.updateChildValues(childUpdates)

Objective-C

注意:此 Firebase 产品不适用于 App Clip 目标。
NSString *key = [[_ref child:@"posts"] childByAutoId].key;
NSDictionary *post = @{@"uid": userID,
                       @"author": username,
                       @"title": title,
                       @"body": body};
NSDictionary *childUpdates = @{[@"/posts/" stringByAppendingString:key]: post,
                               [NSString stringWithFormat:@"/user-posts/%@/%@/", userID, key]: post};
[_ref updateChildValues:childUpdates];

此示例使用childByAutoId在包含/posts/$postid的所有用户的帖子的节点中创建一个帖子,并同时使用getKey()检索密钥。然后可以使用该密钥在/user-posts/$userid/$postid的用户帖子中创建第二个条目。

使用这些路径,您可以通过一次调用updateChildValues来同时更新 JSON 树中的多个位置,例如此示例如何在两个位置创建新帖子。以这种方式进行的同时更新是原子的:要么所有更新都成功,要么所有更新都失败。

添加完成块

如果您想知道您的数据何时被提交,您可以添加一个完成块。 setValueupdateChildValues都采用一个可选的完成块,当写入已提交到数据库时调用该完成块。此侦听器可用于跟踪哪些数据已保存以及哪些数据仍在同步。如果调用不成功,则向侦听器传递一个指示失败原因的错误对象。

迅速

注意:此 Firebase 产品不适用于 App Clip 目标。
ref.child("users").child(user.uid).setValue(["username": username]) {
  (error:Error?, ref:DatabaseReference) in
  if let error = error {
    print("Data could not be saved: \(error).")
  } else {
    print("Data saved successfully!")
  }
}

Objective-C

注意:此 Firebase 产品不适用于 App Clip 目标。
[[[_ref child:@"users"] child:user.uid] setValue:@{@"username": username} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
  if (error) {
    NSLog(@"Data could not be saved: %@", error);
  } else {
    NSLog(@"Data saved successfully.");
  }
}];

删除数据

删除数据的最简单方法是在对该数据位置的引用上调用removeValue

您还可以通过将nil指定为另一个写入操作(例如setValueupdateChildValues )的值来删除。您可以将此技术与updateChildValues一起使用,以在单个 API 调用中删除多个子项。

分离监听器

当您离开ViewController时,观察者不会自动停止同步数据。如果没有正确删除观察者,它会继续将数据同步到本地内存。当不再需要观察者时,通过将关联的FIRDatabaseHandle传递给removeObserverWithHandle方法来移除它。

将回调块添加到引用时,将返回FIRDatabaseHandle 。这些句柄可用于删除回调块。

如果已将多个侦听器添加到数据库引用,则在引发事件时调用每个侦听器。为了停止在该位置同步数据,您必须通过调用removeAllObservers方法删除该位置的所有观察者。

在侦听器上调用removeObserverWithHandleremoveAllObservers不会自动删除在其子节点上注册的侦听器;您还必须跟踪这些引用或句柄以删除它们。

将数据保存为事务

当处理可能被并发修改破坏的数据时,例如增量计数器,您可以使用事务操作。你给这个操作两个参数:一个更新函数和一个可选的完成回调。更新函数将数据的当前状态作为参数,并返回您想要写入的新状态。

例如,在示例社交博客应用程序中,您可以允许用户为帖子加注星标和取消加注星标,并跟踪帖子获得的星数,如下所示:

迅速

注意:此 Firebase 产品不适用于 App Clip 目标。
ref.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in
  if var post = currentData.value as? [String: AnyObject],
    let uid = Auth.auth().currentUser?.uid {
    var stars: [String: Bool]
    stars = post["stars"] as? [String: Bool] ?? [:]
    var starCount = post["starCount"] as? Int ?? 0
    if let _ = stars[uid] {
      // Unstar the post and remove self from stars
      starCount -= 1
      stars.removeValue(forKey: uid)
    } else {
      // Star the post and add self to stars
      starCount += 1
      stars[uid] = true
    }
    post["starCount"] = starCount as AnyObject?
    post["stars"] = stars as AnyObject?

    // Set value and report transaction success
    currentData.value = post

    return TransactionResult.success(withValue: currentData)
  }
  return TransactionResult.success(withValue: currentData)
}) { error, committed, snapshot in
  if let error = error {
    print(error.localizedDescription)
  }
}

Objective-C

注意:此 Firebase 产品不适用于 App Clip 目标。
[ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) {
  NSMutableDictionary *post = currentData.value;
  if (!post || [post isEqual:[NSNull null]]) {
    return [FIRTransactionResult successWithValue:currentData];
  }

  NSMutableDictionary *stars = post[@"stars"];
  if (!stars) {
    stars = [[NSMutableDictionary alloc] initWithCapacity:1];
  }
  NSString *uid = [FIRAuth auth].currentUser.uid;
  int starCount = [post[@"starCount"] intValue];
  if (stars[uid]) {
    // Unstar the post and remove self from stars
    starCount--;
    [stars removeObjectForKey:uid];
  } else {
    // Star the post and add self to stars
    starCount++;
    stars[uid] = @YES;
  }
  post[@"stars"] = stars;
  post[@"starCount"] = @(starCount);

  // Set value and report transaction success
  currentData.value = post;
  return [FIRTransactionResult successWithValue:currentData];
} andCompletionBlock:^(NSError * _Nullable error,
                       BOOL committed,
                       FIRDataSnapshot * _Nullable snapshot) {
  // Transaction completed
  if (error) {
    NSLog(@"%@", error.localizedDescription);
  }
}];

如果多个用户同时为同一个帖子加注星标或客户端的数据过时,使用事务可以防止星标计数不正确。 FIRMutableData类中包含的值最初是客户端最后一个已知的路径值,如果没有,则为nil 。服务器将初始值与其当前值进行比较,如果值匹配则接受交易,或者拒绝交易。如果事务被拒绝,服务器将当前值返回给客户端,客户端使用更新后的值再次运行事务。这会重复,直到交易被接受或进行了太多尝试。

原子服务器端增量

在上面的用例中,我们将两个值写入数据库:为帖子加星/取消星标的用户的 ID,以及增加的星数。如果我们已经知道用户正在为帖子加注星标,我们可以使用原子增量操作而不是事务。

迅速

注意:此 Firebase 产品不适用于 App Clip 目标。
let updates = [
  "posts/\(postID)/stars/\(userID)": true,
  "posts/\(postID)/starCount": ServerValue.increment(1),
  "user-posts/\(postID)/stars/\(userID)": true,
  "user-posts/\(postID)/starCount": ServerValue.increment(1)
] as [String : Any]
Database.database().reference().updateChildValues(updates);

Objective-C

注意:此 Firebase 产品不适用于 App Clip 目标。
NSDictionary *updates = @{[NSString stringWithFormat: @"posts/%@/stars/%@", postID, userID]: @TRUE,
                        [NSString stringWithFormat: @"posts/%@/starCount", postID]: [FIRServerValue increment:@1],
                        [NSString stringWithFormat: @"user-posts/%@/stars/%@", postID, userID]: @TRUE,
                        [NSString stringWithFormat: @"user-posts/%@/starCount", postID]: [FIRServerValue increment:@1]};
[[[FIRDatabase database] reference] updateChildValues:updates];

此代码不使用事务操作,因此如果存在冲突更新,它不会自动重新运行。但是,由于增量操作直接发生在数据库服务器上,因此不会发生冲突。

如果您想检测和拒绝特定于应用程序的冲突,例如用户为他们之前已加注星标的帖子加注星标,您应该为该用例编写自定义安全规则。

离线处理数据

如果客户端失去其网络连接,您的应用程序将继续正常运行。

每个连接到 Firebase 数据库的客户端都维护自己的任何活动数据的内部版本。数据写入时,首先写入到这个本地版本。然后,Firebase 客户端会在“尽力而为”的基础上将该数据与远程数据库服务器和其他客户端同步。

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

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

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

下一步