全新推出 Cloud Firestore(测试版):试用 Firebase 和 Google Cloud Platform 全新推出的规模可扩展且灵活的数据库。详细了解 Cloud Firestore

在 iOS 上读取和写入数据

获取 FIRDatabaseReference

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

Swift

var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

@property (strong, nonatomic) FIRDatabaseReference *ref;

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

读取和写入数据

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

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

基本写入操作

对于基本写入操作,您可以使用 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:withBlockobserveSingleEventOfType:withBlock 方法来观察 FIRDataEventTypeValue 事件。

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

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

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

Swift

refHandle = postRef.observe(DataEventType.value, with: { (snapshot) in
  let postDict = snapshot.value as? [String : AnyObject] ?? [:]
  // ...
})

Objective-C

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

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

读取数据一次

在某些情况下,您需要调用一次回调函数后立即将其移除(例如,在初始化预计不会发生更改的界面元素时)。您可以使用 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

let key = ref.child("posts").childByAutoId().key
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: Dictionary<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。服务器将初始值与其当前值进行比较,如果这两个值匹配,则接受事务,否则将会拒绝事务。如果事务遭拒绝,则服务器会将当前值返回到客户端,然后客户端使用更新后的值再次运行事务。此过程将反复进行,直到事务被接受或尝试次数达到限制为止。

离线写入数据

如果客户端的网络连接中断,您的应用将继续正常运行。

对于所有有效数据,连接到 Firebase 数据库的每个客户端均维护着各自的内部版本。数据在写入时,首先会写入这类本地版本。然后,Firebase 客户端会尽可能将这些数据与远程数据库服务器以及其他客户端同步。

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

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

后续步骤

发送以下问题的反馈:

此网页
Firebase 实时数据库
需要帮助?请访问我们的支持页面