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

(可选)使用 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 Database 和 Cloud Functions 演示。您还应该参阅 Local Emulator Suite 简介

获取 FIRDatabaseReference

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

Swift

var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

@property (strong, nonatomic) FIRDatabaseReference *ref;

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

写入数据

本文将介绍读取和写入 Firebase 数据的基础知识。

Firebase 数据会被写入某个 Database 引用,并通过在该引用上附加异步监听器进行检索。该监听器会针对数据的初始状态触发一次,以后只要数据有更改就会再次触发。

基本写入操作

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

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

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

Swift

self.ref.child("users").child(user.uid).setValue(["username": username])

Objective-C

[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

以这种方式使用 setValue 将重写指定位置(包括所有子节点)的数据。但是,您仍可在不重写整个对象的情况下更新子节点。如果要允许用户更新其个人资料,您可按照如下所示更新用户名:

Swift

self.ref.child("users/\(user.uid)/username").setValue(username)

Objective-C

[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

读取数据

通过侦听值事件读取数据

如需读取某个路径的数据并侦听更改,请使用 FIRDatabaseReferenceobserveEventType:withBlock 观察 FIRDataEventTypeValue 事件。

事件类型 典型用法
FIRDataEventTypeValue 读取并监听对路径中所有内容的更改。

您可以使用 FIRDataEventTypeValue 事件来读取事件发生时给定路径下存在的数据。此方法在附加监听器时触发一次,以后会在每次数据(包括子节点数据)发生更改时再次触发。系统会向事件回调函数传递一个包含该位置中所有数据(包括子节点数据)的 snapshot。如果该位置没有任何数据,当您调用 exists() 时,快照会返回 false;当您读取其 value 属性时,快照会返回 nil

以下示例演示了社交博客应用如何从数据库中检索博文详细信息:

Swift

refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Objective-C

_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

监听器接收到一个 FIRDataSnapshot,其 value 属性中包含事件发生时数据库中指定位置存在的数据。您可以为适当的本机类型赋值,如 NSDictionary。如果该位置不存在任何数据,则 valuenil

读取数据一次

使用 getData() 读取一次

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

通常,您应该使用上述值事件方式读取数据,以接收后端发出的数据更新的通知。这些方法可减少使用量和费用,并经过优化以向用户提供最佳和离线体验。

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

以下示例演示了如何从数据库中检索用户的公开用户名一次:

Swift

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

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 立即从本地磁盘缓存获取数据。

对于只需加载一次且预计不会频繁变化或不需要主动侦听的数据,这种方法非常有用。例如,上述示例中的博客应用使用了此方法在用户开始撰写新博文时加载其个人资料:

Swift

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

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 时,您可以通过为键指定路径来更新较低层级的子节点值。如果为了更好地实现扩缩而将数据存储在多个位置,则可使用数据扇出更新这些数据的所有实例。例如,社交博客应用可能需要创建一篇博文,同时将其更新到最新的活动 Feed 和发布用户的活动 Feed。为此,该博客应用需要使用如下代码:

Swift

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

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 均支持完成代码块(您可视需要添加),当写入的数据被提交到数据库后,系统就会调用该完成代码块。这个监听器可用于跟踪哪些数据已保存,以及哪些数据仍在同步。如果调用失败,则系统将为该监听器传递一个错误对象,说明失败的原因。

Swift

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

[[[_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 不会自动移除在子节点上注册的监听器;您还必须跟踪这些引用或句柄才能将其移除。

将数据另存为事务

处理可能因并发修改而损坏的数据(例如,增量计数器)时,您可以使用事务操作。您需要为此操作提供两个参数:更新函数和可选的完成回调函数。更新函数将数据的当前状态视为参数,并返回您要写入的新目标状态。

例如,在示例社交博客应用中,您可以允许用户对博文加星和取消加星,并跟踪博文获得的星数,如下所示:

Swift

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

[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,以及递增的星标数。如果我们已经知道用户正在为信息加星标,我们可以使用原子化增量操作,而不是事务。

Swift

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

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

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

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

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

后续步骤