このドキュメントでは、Firebase データの読み取りと書き込みの基本について説明します。
Firebase データはFirebaseDatabase
参照に書き込まれ、非同期リスナーを参照にアタッチすることで取得されます。リスナーは、データの初期状態に対して 1 回トリガーされ、データが変更されるたびに再度トリガーされます。
(省略可)Firebase Local Emulator Suite でプロトタイプを作成してテストする
アプリが Realtime Database に対して読み取りと書き込みを行う方法について説明する前に、Realtime Database 機能のプロトタイプ作成とテストに使用できる一連のツール、Firebase Local Emulator Suite を紹介しましょう。さまざまなデータ モデルを試したり、セキュリティ ルールを最適化したり、バックエンドと対話するための最も費用対効果の高い方法を見つけようとしている場合、ライブ サービスをデプロイせずにローカルで作業できることは素晴らしいアイデアです。
Realtime Database エミュレーターは、ローカル エミュレーター スイートの一部であり、アプリがエミュレートされたデータベースのコンテンツと構成、および必要に応じてエミュレートされたプロジェクト リソース (関数、他のデータベース、およびセキュリティ ルール) とやり取りできるようにします。
Realtime Database エミュレーターを使用するには、いくつかの手順を実行する必要があります。
- アプリのテスト構成にコード行を追加して、エミュレーターに接続します。
- ローカル プロジェクト ディレクトリのルートから、
firebase emulators:start
を実行します。 - 通常どおり Realtime Database プラットフォーム SDK を使用するか、Realtime Database REST API を使用して、アプリのプロトタイプ コードから呼び出しを行います。
Realtime Database と Cloud Functions に関する詳細なチュートリアルが利用可能です。 Local Emulator Suite Introductionも参照してください。
DatabaseReference を取得する
データベースからデータを読み書きするには、 DatabaseReference
のインスタンスが必要です。
Kotlin+KTX
private lateinit var database: DatabaseReference // ... database = Firebase.database.reference
Java
private DatabaseReference mDatabase; // ... mDatabase = FirebaseDatabase.getInstance().getReference();
書き込みデータ
基本的な書き込み操作
基本的な書き込み操作では、 setValue()
を使用して指定した参照にデータを保存し、そのパスにある既存のデータを置き換えることができます。この方法を使用すると、次のことができます。
- 使用可能な JSON タイプに対応するパス タイプは次のとおりです。
-
String
-
Long
-
Double
-
Boolean
-
Map<String, Object>
-
List<Object>
-
- カスタム Java オブジェクトを渡す (それを定義するクラスに、引数を取らず、割り当てられるプロパティのパブリック ゲッターがあるデフォルト コンストラクターがある場合)。
Java オブジェクトを使用する場合、オブジェクトのコンテンツはネストされた方法で子の場所に自動的にマップされます。また、通常、Java オブジェクトを使用すると、コードが読みやすくなり、保守が容易になります。たとえば、基本的なユーザー プロファイルを持つアプリがある場合、 User
オブジェクトは次のようになります。
Kotlin+KTX
@IgnoreExtraProperties data class User(val username: String? = null, val email: String? = null) { // Null default values create a no-argument default constructor, which is needed // for deserialization from a DataSnapshot. }
Java
@IgnoreExtraProperties public class User { public String username; public String email; public User() { // Default constructor required for calls to DataSnapshot.getValue(User.class) } public User(String username, String email) { this.username = username; this.email = email; } }
次のようにsetValue()
を使用してユーザーを追加できます。
Kotlin+KTX
fun writeNewUser(userId: String, name: String, email: String) { val user = User(name, email) database.child("users").child(userId).setValue(user) }
Java
public void writeNewUser(String userId, String name, String email) { User user = new User(name, email); mDatabase.child("users").child(userId).setValue(user); }
このようにsetValue()
を使用すると、子ノードを含め、指定された場所のデータが上書きされます。ただし、オブジェクト全体を書き換えなくても子を更新できます。ユーザーがプロファイルを更新できるようにしたい場合は、次のようにユーザー名を更新できます。
Kotlin+KTX
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
データの読み取り
永続リスナーを使用してデータを読み取る
パスでデータを読み取り、変更をリッスンするには、 addValueEventListener()
メソッドを使用してValueEventListener
をDatabaseReference
に追加します。
リスナー | イベント コールバック | 典型的な使用法 |
---|---|---|
ValueEventListener | onDataChange() | パスの内容全体に対する変更を読み取り、リッスンします。 |
onDataChange()
メソッドを使用して、特定のパスにあるコンテンツの静的スナップショットを読み取ることができます。これは、イベント時に存在していたものです。このメソッドは、リスナーがアタッチされたときに 1 回トリガーされ、子を含むデータが変更されるたびに再度トリガーされます。イベント コールバックには、子データを含む、その場所にあるすべてのデータを含むスナップショットが渡されます。データがない場合、 exists()
を呼び出すとスナップショットはfalse
を返し、 getValue()
を呼び出すとnull
を返します。
次の例は、データベースから投稿の詳細を取得するソーシャル ブログ アプリケーションを示しています。
Kotlin+KTX
val postListener = object : ValueEventListener { override fun onDataChange(dataSnapshot: DataSnapshot) { // Get Post object and use the values to update the UI val post = dataSnapshot.getValue<Post>() // ... } override fun onCancelled(databaseError: DatabaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()) } } postReference.addValueEventListener(postListener)
Java
ValueEventListener postListener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { // Get Post object and use the values to update the UI Post post = dataSnapshot.getValue(Post.class); // .. } @Override public void onCancelled(DatabaseError databaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()); } }; mPostReference.addValueEventListener(postListener);
リスナーは、イベント時にデータベース内の指定された場所にあるデータを含むDataSnapshot
を受け取ります。スナップショットでgetValue()
を呼び出すと、データの Java オブジェクト表現が返されます。その場所にデータが存在しない場合、 getValue()
を呼び出すとnull
が返されます。
この例では、 ValueEventListener
は、読み取りがキャンセルされた場合に呼び出されるonCancelled()
メソッドも定義します。たとえば、クライアントが Firebase データベースの場所から読み取る権限を持っていない場合、読み取りをキャンセルできます。このメソッドには、障害が発生した理由を示すDatabaseError
オブジェクトが渡されます。
データを 1 回読み取る
get() を使用して 1 回読み取る
SDK は、アプリがオンラインかオフラインかに関係なく、データベース サーバーとの対話を管理するように設計されています。
通常、上記のValueEventListener
手法を使用してデータを読み取り、バックエンドからデータの更新の通知を受け取る必要があります。リスナーの手法により、使用量と請求額が削減され、オンラインとオフラインの両方でユーザーに最高のエクスペリエンスを提供するように最適化されます。
データが一度だけ必要な場合は、 get()
を使用してデータベースからデータのスナップショットを取得できます。何らかの理由でget()
がサーバーの値を返すことができない場合、クライアントはローカル ストレージ キャッシュを調べ、それでも値が見つからない場合はエラーを返します。
get()
を不必要に使用すると、帯域幅の使用が増加し、パフォーマンスが低下する可能性があります。これは、上記のようにリアルタイム リスナーを使用することで防ぐことができます。
Kotlin+KTX
mDatabase.child("users").child(userId).get().addOnSuccessListener {
Log.i("firebase", "Got value ${it.value}")
}.addOnFailureListener{
Log.e("firebase", "Error getting data", it)
}
Java
mDatabase.child("users").child(userId).get().addOnCompleteListener(new OnCompleteListener<DataSnapshot>() {
@Override
public void onComplete(@NonNull Task<DataSnapshot> task) {
if (!task.isSuccessful()) {
Log.e("firebase", "Error getting data", task.getException());
}
else {
Log.d("firebase", String.valueOf(task.getResult().getValue()));
}
}
});
リスナーを使用して一度だけ読み取る
場合によっては、サーバーで更新された値を確認するのではなく、ローカル キャッシュの値をすぐに返したい場合があります。そのような場合、 addListenerForSingleValueEvent
を使用して、ローカル ディスク キャッシュからデータをすぐに取得できます。
これは、一度だけロードする必要があり、頻繁に変更されることやアクティブなリッスンを必要としないことが予想されるデータに役立ちます。たとえば、前の例のブログ アプリは、このメソッドを使用して、ユーザーが新しい投稿の作成を開始するときにユーザーのプロファイルを読み込みます。
データの更新または削除
特定のフィールドを更新する
他の子ノードを上書きせずにノードの特定の子に同時に書き込むには、 updateChildren()
メソッドを使用します。
updateChildren()
を呼び出すときに、キーのパスを指定することで、下位レベルの子の値を更新できます。スケーリングを改善するためにデータが複数の場所に保存されている場合は、データ ファンアウトを使用してそのデータのすべてのインスタンスを更新できます。たとえば、ソーシャル ブログ アプリには、次のようなPost
クラスがあります。
Kotlin+KTX
@IgnoreExtraProperties data class Post( var uid: String? = "", var author: String? = "", var title: String? = "", var body: String? = "", var starCount: Int = 0, var stars: MutableMap<String, Boolean> = HashMap() ) { @Exclude fun toMap(): Map<String, Any?> { return mapOf( "uid" to uid, "author" to author, "title" to title, "body" to body, "starCount" to starCount, "stars" to stars ) } }
Java
@IgnoreExtraProperties public class Post { public String uid; public String author; public String title; public String body; public int starCount = 0; public Map<String, Boolean> stars = new HashMap<>(); public Post() { // Default constructor required for calls to DataSnapshot.getValue(Post.class) } public Post(String uid, String author, String title, String body) { this.uid = uid; this.author = author; this.title = title; this.body = body; } @Exclude public Map<String, Object> toMap() { HashMap<String, Object> result = new HashMap<>(); result.put("uid", uid); result.put("author", author); result.put("title", title); result.put("body", body); result.put("starCount", starCount); result.put("stars", stars); return result; } }
投稿を作成し、それを最近のアクティビティ フィードと投稿ユーザーのアクティビティ フィードに同時に更新するには、ブログ アプリケーションで次のようなコードを使用します。
Kotlin+KTX
private fun writeNewPost(userId: String, username: String, title: String, body: String) { // Create new post at /user-posts/$userid/$postid and at // /posts/$postid simultaneously val key = database.child("posts").push().key if (key == null) { Log.w(TAG, "Couldn't get push key for posts") return } val post = Post(userId, username, title, body) val postValues = post.toMap() val childUpdates = hashMapOf<String, Any>( "/posts/$key" to postValues, "/user-posts/$userId/$key" to postValues ) database.updateChildren(childUpdates) }
Java
private void writeNewPost(String userId, String username, String title, String body) { // Create new post at /user-posts/$userid/$postid and at // /posts/$postid simultaneously String key = mDatabase.child("posts").push().getKey(); Post post = new Post(userId, username, title, body); Map<String, Object> postValues = post.toMap(); Map<String, Object> childUpdates = new HashMap<>(); childUpdates.put("/posts/" + key, postValues); childUpdates.put("/user-posts/" + userId + "/" + key, postValues); mDatabase.updateChildren(childUpdates); }
この例では、 push()
を使用して/posts/$postid
にあるすべてのユーザーの投稿を含むノードに投稿を作成し、同時にgetKey()
でキーを取得します。このキーを使用して、 /user-posts/$userid/$postid
にあるユーザーの投稿に 2 番目のエントリを作成できます。
これらのパスを使用すると、 updateChildren()
を 1 回呼び出すだけで、JSON ツリー内の複数の場所に同時に更新を実行できます。たとえば、この例では両方の場所に新しい投稿を作成しています。この方法で行われる同時更新はアトミックです。すべての更新が成功するか、すべての更新が失敗します。
完了コールバックを追加する
データがいつコミットされたかを知りたい場合は、完了リスナーを追加できます。 setValue()
とupdateChildren()
はどちらも、書き込みがデータベースに正常にコミットされたときに呼び出されるオプションの完了リスナーを取ります。呼び出しが失敗した場合、失敗の原因を示すエラー オブジェクトがリスナーに渡されます。
Kotlin+KTX
database.child("users").child(userId).setValue(user) .addOnSuccessListener { // Write was successful! // ... } .addOnFailureListener { // Write failed // ... }
Java
mDatabase.child("users").child(userId).setValue(user) .addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { // Write was successful! // ... } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Write failed // ... } });
データを削除する
データを削除する最も簡単な方法は、そのデータの場所への参照に対してremoveValue()
を呼び出すことです。
setValue()
やupdateChildren()
) などの別の書き込み操作の値としてnull
を指定して削除することもできます。この手法をupdateChildren()
で使用して、1 回の API 呼び出しで複数の子を削除できます。
リスナーを切り離す
Firebase データベース参照でremoveEventListener()
メソッドを呼び出すと、コールバックが削除されます。
リスナーがデータの場所に複数回追加されている場合、イベントごとに複数回呼び出され、完全に削除するには同じ回数デタッチする必要があります。
親リスナーでremoveEventListener()
を呼び出しても、その子ノードに登録されているリスナーは自動的に削除されません。子リスナーでremoveEventListener()
を呼び出して、コールバックを削除する必要もあります。
データをトランザクションとして保存
増分カウンターなど、同時変更によって破損する可能性があるデータを操作する場合は、トランザクション操作を使用できます。この操作には、更新関数とオプションの完了コールバックの 2 つの引数を指定します。更新関数は、データの現在の状態を引数として取り、書き込みたい新しい望ましい状態を返します。新しい値が正常に書き込まれる前に別のクライアントがその場所に書き込むと、更新関数が新しい現在の値で再度呼び出され、書き込みが再試行されます。
たとえば、ソーシャル ブログ アプリの例では、次のように、ユーザーが投稿にスターを付けたり、スターを外したり、投稿が獲得したスターの数を追跡したりできるようにすることができます。
Kotlin+KTX
private fun onStarClicked(postRef: DatabaseReference) { // ... postRef.runTransaction(object : Transaction.Handler { override fun doTransaction(mutableData: MutableData): Transaction.Result { val p = mutableData.getValue(Post::class.java) ?: return Transaction.success(mutableData) if (p.stars.containsKey(uid)) { // Unstar the post and remove self from stars p.starCount = p.starCount - 1 p.stars.remove(uid) } else { // Star the post and add self to stars p.starCount = p.starCount + 1 p.stars[uid] = true } // Set value and report transaction success mutableData.value = p return Transaction.success(mutableData) } override fun onComplete( databaseError: DatabaseError?, committed: Boolean, currentData: DataSnapshot? ) { // Transaction completed Log.d(TAG, "postTransaction:onComplete:" + databaseError!!) } }) }
Java
private void onStarClicked(DatabaseReference postRef) { postRef.runTransaction(new Transaction.Handler() { @NonNull @Override public Transaction.Result doTransaction(@NonNull MutableData mutableData) { Post p = mutableData.getValue(Post.class); if (p == null) { return Transaction.success(mutableData); } if (p.stars.containsKey(getUid())) { // Unstar the post and remove self from stars p.starCount = p.starCount - 1; p.stars.remove(getUid()); } else { // Star the post and add self to stars p.starCount = p.starCount + 1; p.stars.put(getUid(), true); } // Set value and report transaction success mutableData.setValue(p); return Transaction.success(mutableData); } @Override public void onComplete(DatabaseError databaseError, boolean committed, DataSnapshot currentData) { // Transaction completed Log.d(TAG, "postTransaction:onComplete:" + databaseError); } }); }
トランザクションを使用すると、複数のユーザーが同じ投稿に同時にスターを付けたり、クライアントのデータが古い場合に、スター カウントが正しくなくなるのを防ぐことができます。トランザクションが拒否された場合、サーバーは現在の値をクライアントに返し、クライアントは更新された値でトランザクションを再度実行します。これは、トランザクションが受け入れられるか、試行回数が多すぎるまで繰り返されます。
サーバー側のアトミックインクリメント
上記の使用例では、データベースに 2 つの値を書き込んでいます。投稿に星を付けた/星を外したユーザーの ID と、増加した星の数です。ユーザーが投稿にスターを付けていることがすでにわかっている場合は、トランザクションの代わりにアトミック インクリメント操作を使用できます。
Kotlin+KTX
private fun onStarClicked(uid: String, key: String) { val updates: MutableMap<String, Any> = hashMapOf( "posts/$key/stars/$uid" to true, "posts/$key/starCount" to ServerValue.increment(1), "user-posts/$uid/$key/stars/$uid" to true, "user-posts/$uid/$key/starCount" to ServerValue.increment(1) ) database.updateChildren(updates) }
Java
private void onStarClicked(String uid, String key) { Map<String, Object> updates = new HashMap<>(); updates.put("posts/"+key+"/stars/"+uid, true); updates.put("posts/"+key+"/starCount", ServerValue.increment(1)); updates.put("user-posts/"+uid+"/"+key+"/stars/"+uid, true); updates.put("user-posts/"+uid+"/"+key+"/starCount", ServerValue.increment(1)); mDatabase.updateChildren(updates); }
このコードはトランザクション操作を使用しないため、競合する更新がある場合に自動的に再実行されることはありません。ただし、インクリメント操作はデータベース サーバーで直接行われるため、競合が発生する可能性はありません。
ユーザーが以前にスターを付けた投稿にスターを付けるなど、アプリケーション固有の競合を検出して拒否する場合は、そのユース ケース用のカスタム セキュリティ ルールを作成する必要があります。
オフラインでデータを操作する
クライアントがネットワーク接続を失った場合でも、アプリは正常に機能し続けます。
Firebase データベースに接続されているすべてのクライアントは、リスナーが使用されているデータ、またはサーバーとの同期を維持するようにフラグが設定されているデータの独自の内部バージョンを保持します。データの読み取りまたは書き込み時には、このローカル バージョンのデータが最初に使用されます。次に、Firebase クライアントは、そのデータをリモート データベース サーバーおよび他のクライアントと「ベスト エフォート」ベースで同期します。
その結果、データベースへのすべての書き込みは、サーバーとの対話の前に、すぐにローカル イベントをトリガーします。これは、ネットワークの遅延や接続に関係なく、アプリの応答性が維持されることを意味します。
接続が再確立されると、アプリは適切な一連のイベントを受け取り、クライアントが現在のサーバーの状態と同期するようにします。カスタム コードを記述する必要はありません。
オフラインの動作については、オンラインとオフラインの機能の詳細をご覧ください で詳しく説明します。