(省略可)Firebase Local Emulator Suite でプロトタイピングとテストを行う
アプリが Realtime Database との間でどのようにデータを読み取り / 書き込みするのかを説明する前に、Realtime Database の機能のプロトタイピングとテストに使用できるツールである Firebase Local Emulator Suite をご紹介します。異なるデータモデルの試行や、セキュリティ ルールの最適化、あるいはバックエンドとのやり取りで費用対効果の高い方法の検出を行う場合は、ライブサービスをデプロイせずにローカルで作業できるようにすると、大きなメリットが得られます。
Realtime Database エミュレータは Local Emulator Suite の一部であり、これを使用すると、アプリはエミュレートしたデータベースのコンテンツや構成とやり取りできるほか、エミュレートされたプロジェクトのリソース(関数、他のデータベース、セキュリティ ルール)とも任意でやり取りできます。
いくつかの手順を実施するだけで、Realtime Database エミュレータを使用できます。
- アプリのテスト構成にコード行を追加して、エミュレータに接続します。
- ローカル プロジェクトのディレクトリのルートから、
firebase emulators:start
を実行します。 - 通常どおり 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
参照に書き込まれ、取得する際にはその参照に非同期リスナーをアタッチします。リスナーはデータの初期状態で 1 回トリガーされます。さらに、データが変更されると、そのたびに再度トリガーされます。
基本的な書き込みオペレーション
基本的な書き込みオペレーションは、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
をこの方法で使用すると、特定の場所にあるデータ(子ノードも含む)が上書きされます。ただし、オブジェクト全体を書き換えずに子を更新することもできます。ユーザーに自分のプロフィールの更新を許可する場合、次のように username を更新できます。
Swift
self.ref.child("users/\(user.uid)/username").setValue(username)
Objective-C
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
データを読み取る
値イベントをリッスンしてデータを読み取る
パスにあるデータを読み取り、変更をリッスンするには、FIRDatabaseReference
の observeEventType:withBlock
を使用して FIRDataEventTypeValue
イベントを監視します。
イベントの種類 | 一般的な使用方法 |
---|---|
FIRDataEventTypeValue |
パスのコンテンツ全体に対する変更の読み取りとリッスンを行います。 |
FIRDataEventTypeValue
イベントを使用して、特定のパスにあるデータを、イベントの発生時に存在していたとおりに読み取ることができます。このメソッドはリスナーがアタッチされたときに 1 回トリガーされます。さらに、データ(子も含む)が変更されると、そのたびに再トリガーされます。イベントのコールバックには、その場所にあるすべてのデータ(子のデータも含む)を含んでいる 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
など、適切なネイティブ型に割り当てることができます。その場所にデータが存在しない場合、value
は nil
です。
データを 1 回読み取る
getData() を使用して 1 回読み取る
SDK は、アプリがオンラインかオフラインかに関係なく、データベース サーバーとのやり取りを管理するように設計されています。
通常は、上述の値イベント手法を使用してデータを読み取り、データの更新に関する通知をバックエンドから受け取ります。これらの手法を利用することで、データの使用量と請求額を削減でき、オンラインとオフラインのどちらでも最高のユーザー エクスペリエンスを実現できます。
データが 1 回だけ必要な場合は、getData()
を使用してデータベースからデータのスナップショットを取得します。なんらかの理由で getData()
がサーバー値を返せない場合は、クライアントがローカル ストレージ キャッシュを調べ、それでも値が見つからなければエラーを返します。
次の例は、データベースからユーザーの公開ユーザー名を 1 回だけ取得する方法を示しています。
Swift
do { let snapshot = try await ref.child("users/\(uid)/username").getData() let userName = snapshot.value as? String ?? "Unknown" } catch { print(error) }
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()
を必要以上に使用すると、帯域幅の使用が増加し、パフォーマンスの低下を招くおそれがあります。ただし、上記のリアルタイム リスナーを使用することで、これを回避できます。
オブザーバーを使用してデータを 1 回読み取る
更新された値をサーバーで確認するのではなく、値をローカル キャッシュから直ちに返したい場合があります。そのような場合は、observeSingleEventOfType
を使用してローカル ディスク キャッシュから直ちにデータを取得できます。
これは 1 回読み込む必要があるだけで頻繁な変更やアクティブなリッスンを行うことは想定していないデータに対して有用です。たとえば、前の例にあるブログアプリでは、このメソッドを使用して、ユーザーが新しい投稿を作成し始めたときにユーザーのプロフィールを読み込んでいます。
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
の呼び出し時に、キーのパスを指定して下位レベルの子の値を更新できます。スケーラビリティを向上させるためにデータが複数の場所に保存されている場合、データのファンアウトを使用してそのデータのすべてのインスタンスを更新できます。たとえば、ソーシャル ブログ アプリで、投稿を作成して、それと同時にその投稿から最近のアクティビティ フィードと投稿ユーザーのアクティビティ フィードを更新するとします。これを実現するには、ブログアプリで次のようなコードを使用します。
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
を 1 回呼び出すだけで JSON ツリー内の複数の場所に対して更新を同時に実行できます。この方法による同時更新はアトミック(不可分)です。つまり、すべての更新が成功するか、すべての更新が失敗するかのどちらかです。
完了ブロックの追加
データがいつ commit されたのかを把握するには、完了ブロックを追加します。setValue
と updateChildValues
はどちらもオプションとして完了ブロックを取ります。このブロックは、データベースに書き込みが commit されたときに呼び出されます。このリスナーは、どのデータが保存され、どのデータが同期中かを追跡する場合に役立ちます。呼び出しに失敗すると、障害が発生した理由を示すエラー オブジェクトがリスナーに渡されます。
Swift
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
[[[_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
を呼び出すことです。
また、他の書き込みオペレーション(setValue
や updateChildValues
など)の値として nil
を指定する方法でも削除できます。この方法と updateChildValues
を併用すると、API を 1 回呼び出すだけで複数の子を削除できます。
リスナーのデタッチ
ViewController
から離れても、オブザーバーはデータの同期を自動的に停止するわけではありません。オブザーバーが適切に削除されないと、オブザーバーによってデータがローカルメモリに同期され続けます。オブザーバーが不要になったときは、関連付けられている FIRDatabaseHandle
を removeObserverWithHandle
メソッドに渡してオブザーバーを削除してください。
参照にコールバック ブロックを追加すると、FIRDatabaseHandle
が返されます。これらのハンドルを使用して、コールバック ブロックを削除できます。
データベース参照に複数のリスナーが追加されている場合、イベントが発生したときにそれぞれのリスナーが呼び出されます。ある場所でのデータの同期を停止するには、removeAllObservers
メソッドを呼び出して、その場所のすべてのオブザーバーを削除する必要があります。
リスナーで removeObserverWithHandle
または removeAllObservers
を呼び出しても、その子ノードに登録されているリスナーは自動的に削除されません。これらの参照やハンドルも追跡して削除する必要があります。
トランザクションとしてのデータの保存
増分カウンタなど、同時変更によって破損する可能性があるデータを操作する場合は、トランザクション オペレーションを使用します。このオペレーションには、2 つの引数(update 関数とオプションの完了コールバック)を与えます。update 関数はデータの現在の状態を引数として取り、書き込みたい新しい状態を返します。
たとえば、このソーシャル ブログ アプリの例では、次のようにして、投稿にスターを付ける / 投稿のスターを取り消す操作をユーザーに許可し、投稿で得られたスターの数を追跡できます。
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
になります。サーバーは、この初期値をその現在の値と比較し、値が一致した場合はトランザクションを受け入れ、そうでない場合は拒否します。トランザクションが拒否された場合、サーバーは現在の値をクライアントに返します。クライアントは更新された値でトランザクションを再度実行します。この処理は、トランザクションが受け入れられるか、試行が上限の回数に達するまで繰り返されます。
サーバーサイドのアトミックなインクリメント
上のユースケースでは 2 つの値をデータベースに書き込みます。投稿にスターを付ける / スターを外すユーザーの 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 クライアントは、「ベスト エフォート」ベースでそのデータをリモート データベース サーバーや他のクライアントと同期します。
その結果、データベースへの書き込みが発生すると、実際にサーバーへデータが書き込まれるよりも早く、ローカル イベントが直ちにトリガーされます。つまり、ネットワークのレイテンシや接続に関係なく、アプリは応答性の高い状態を維持します。
接続が再確立されると、アプリは適切な一連のイベントを受け取り、クライアントが現在のサーバー状態と同期されます。この処理のためにカスタムコードを記述する必要はありません。
オフラインの動作については、オンライン機能とオフラインの機能の詳細で説明します。