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 DatabaseCloud Functions に関するチュートリアルをご覧ください。Local Emulator Suite の概要もご覧ください。

FIRDatabaseReference の取得

データベースでデータの読み書きを行うには、FIRDatabaseReference のインスタンスが必要です。

Swift

注: この 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 参照に書き込まれ、取得する際にはその参照に非同期リスナーをアタッチします。リスナーはデータの初期状態で 1 回トリガーされます。さらに、データが変更されると、そのたびに再度トリガーされます。

基本的な書き込みオペレーション

基本的な書き込みオペレーションは、setValue を使用してデータを特定の参照に保存できます。そのパスにある既存のデータが置換されます。このメソッドを使用して行える操作は次のとおりです。

  • 次のような、使用可能な JSON 型に対応する型を渡します。
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

たとえば、次のように setValue でユーザーを追加できます。

Swift

注: この 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 をこの方法で使用すると、特定の場所にあるデータ(子ノードも含む)が上書きされます。ただし、オブジェクト全体を書き換えずに子を更新することもできます。ユーザーに自分のプロフィールの更新を許可する場合、次のように username を更新できます。

Swift

注: この 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];

データを読み取る

値イベントをリッスンしてデータを読み取る

パスにあるデータを読み取り、変更をリッスンするには、FIRDatabaseReferenceobserveEventType:withBlock を使用して FIRDataEventTypeValue イベントを監視します。

イベントの種類 一般的な使用方法
FIRDataEventTypeValue パスのコンテンツ全体に対する変更の読み取りとリッスンを行います。

FIRDataEventTypeValue イベントを使用して、特定のパスにあるデータを、イベントの発生時に存在していたとおりに読み取ることができます。このメソッドはリスナーがアタッチされたときに 1 回トリガーされます。さらに、データ(子も含む)が変更されると、そのたびに再トリガーされます。イベントのコールバックには、その場所にあるすべてのデータ(子のデータも含む)を含んでいる snapshot が渡されます。データが存在しない場合、スナップショットから返されるのは、exists() を呼び出した場合は false で、value プロパティを読み取った場合は nil です。

次の例は、データベースから投稿の詳細を取得するソーシャル ブログ アプリケーションを示しています。

Swift

注: この 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;
  // ...
}];

リスナーは FIRDataSnapshot を受信します。その value プロパティには、イベントのときにデータベース内の指定された場所にあったデータが含まれています。この値は、NSDictionary など、適切なネイティブ型に割り当てることができます。その場所にデータが存在しない場合、valuenil です。

データを 1 回読み取る

getData() を使用して 1 回読み取る

SDK は、アプリがオンラインかオフラインかに関係なく、データベース サーバーとのやり取りを管理するように設計されています。

通常は、上述の値イベント手法を使用してデータを読み取り、データの更新に関する通知をバックエンドから受け取ります。これらの手法を利用することで、データの使用量と請求額を削減でき、オンラインとオフラインのどちらでも最高のユーザー エクスペリエンスを実現できます。

データが 1 回だけ必要な場合は、getData() を使用してデータベースからデータのスナップショットを取得します。なんらかの理由で getData() がサーバー値を返せない場合は、クライアントがローカル ストレージ キャッシュを調べ、それでも値が見つからなければエラーを返します。

次の例は、データベースからユーザーの公開ユーザー名を 1 回だけ取得する方法を示しています。

Swift

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
do {
  let snapshot = try await ref.child("users/\(uid)/username").getData()
  let userName = snapshot.value as? String ?? "Unknown"
} catch {
  print(error)
}

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() を必要以上に使用すると、帯域幅の使用が増加し、パフォーマンスの低下を招くおそれがあります。ただし、上記のリアルタイム リスナーを使用することで、これを回避できます。

オブザーバーを使用してデータを 1 回読み取る

更新された値をサーバーで確認するのではなく、値をローカル キャッシュから直ちに返したい場合があります。そのような場合は、observeSingleEventOfType を使用してローカル ディスク キャッシュから直ちにデータを取得できます。

これは 1 回読み込む必要があるだけで頻繁な変更やアクティブなリッスンを行うことは想定していないデータに対して有用です。たとえば、前の例にあるブログアプリでは、このメソッドを使用して、ユーザーが新しい投稿を作成し始めたときにユーザーのプロフィールを読み込んでいます。

Swift

注: この 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 の呼び出し時に、キーのパスを指定して下位レベルの子の値を更新できます。スケーラビリティを向上させるためにデータが複数の場所に保存されている場合、データのファンアウトを使用してそのデータのすべてのインスタンスを更新できます。たとえば、ソーシャル ブログ アプリで、投稿を作成して、それと同時にその投稿から最近のアクティビティ フィードと投稿ユーザーのアクティビティ フィードを更新するとします。これを実現するには、ブログアプリで次のようなコードを使用します。

Swift

注: この 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 を 1 回呼び出すだけで JSON ツリー内の複数の場所に対して更新を同時に実行できます。この方法による同時更新はアトミック(不可分)です。つまり、すべての更新が成功するか、すべての更新が失敗するかのどちらかです。

完了ブロックの追加

データがいつ commit されたのかを把握するには、完了ブロックを追加します。setValueupdateChildValues はどちらもオプションとして完了ブロックを取ります。このブロックは、データベースに書き込みが commit されたときに呼び出されます。このリスナーは、どのデータが保存され、どのデータが同期中かを追跡する場合に役立ちます。呼び出しに失敗すると、障害が発生した理由を示すエラー オブジェクトがリスナーに渡されます。

Swift

注: この Firebase プロダクトは、App Clip ターゲットでは使用できません。
do {
  try await ref.child("users").child(user.uid).setValue(["username": username])
  print("Data saved successfully!")
} catch {
  print("Data could not be saved: \(error).")
}

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 を呼び出すことです。

また、他の書き込みオペレーション(setValueupdateChildValues など)の値として nil を指定する方法でも削除できます。この方法と updateChildValues を併用すると、API を 1 回呼び出すだけで複数の子を削除できます。

リスナーのデタッチ

ViewController から離れても、オブザーバーはデータの同期を自動的に停止するわけではありません。オブザーバーが適切に削除されないと、オブザーバーによってデータがローカルメモリに同期され続けます。オブザーバーが不要になったときは、関連付けられている FIRDatabaseHandleremoveObserverWithHandle メソッドに渡してオブザーバーを削除してください。

参照にコールバック ブロックを追加すると、FIRDatabaseHandle が返されます。これらのハンドルを使用して、コールバック ブロックを削除できます。

データベース参照に複数のリスナーが追加されている場合、イベントが発生したときにそれぞれのリスナーが呼び出されます。ある場所でのデータの同期を停止するには、removeAllObservers メソッドを呼び出して、その場所のすべてのオブザーバーを削除する必要があります。

リスナーで removeObserverWithHandle または removeAllObservers を呼び出しても、その子ノードに登録されているリスナーは自動的に削除されません。これらの参照やハンドルも追跡して削除する必要があります。

トランザクションとしてのデータの保存

増分カウンタなど、同時変更によって破損する可能性があるデータを操作する場合は、トランザクション オペレーションを使用します。このオペレーションには、2 つの引数(update 関数とオプションの完了コールバック)を与えます。update 関数はデータの現在の状態を引数として取り、書き込みたい新しい状態を返します。

たとえば、このソーシャル ブログ アプリの例では、次のようにして、投稿にスターを付ける / 投稿のスターを取り消す操作をユーザーに許可し、投稿で得られたスターの数を追跡できます。

Swift

注: この 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 になります。サーバーは、この初期値をその現在の値と比較し、値が一致した場合はトランザクションを受け入れ、そうでない場合は拒否します。トランザクションが拒否された場合、サーバーは現在の値をクライアントに返します。クライアントは更新された値でトランザクションを再度実行します。この処理は、トランザクションが受け入れられるか、試行が上限の回数に達するまで繰り返されます。

サーバーサイドのアトミックなインクリメント

上のユースケースでは 2 つの値をデータベースに書き込みます。投稿にスターを付ける / スターを外すユーザーの ID と、インクリメントされたスターの数です。ユーザーが投稿にスターを付けていることがわかっている場合は、トランザクションではなくアトミックなインクリメント オペレーションを使用できます。

Swift

注: この 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 クライアントは、「ベスト エフォート」ベースでそのデータをリモート データベース サーバーや他のクライアントと同期します。

その結果、データベースへの書き込みが発生すると、実際にサーバーへデータが書き込まれるよりも早く、ローカル イベントが直ちにトリガーされます。つまり、ネットワークのレイテンシや接続に関係なく、アプリは応答性の高い状態を維持します。

接続が再確立されると、アプリは適切な一連のイベントを受け取り、クライアントが現在のサーバー状態と同期されます。この処理のためにカスタムコードを記述する必要はありません。

オフラインの動作については、オンライン機能とオフラインの機能の詳細で説明します。

次のステップ