前回の連載では、Firebaseに簡単なデータを保存し、それを実際にAndroidクライアントから読み出してみました。今回は、より詳細なデータの取得方法と、取得するデータのソート、取得するデータ数のリミットや範囲といった複雑なクエリについて解説します。
今回は例として、リアルタイムチャットアプリケーションを作成すると想定して以下の様なデータを用意し、さまざまな方法で読み出してみたいと思います。
/messages
以下に、チャットのメッセージ情報を{"sender":"送信者", "body":"本文"}
の一覧形式で格納
/counts
以下に、どのユーザが何通メッセージを受信したかという情報を{"ユーザ名":数}
という形式で格納
イベントとリスナ
前回の「イベント駆動型プログラミング」の節でご説明したように、Firebaseではデータが欲しいタイミングでプログラマが能動的に取りに行く「Pull型」ではなく、データが追加されたり変更されたタイミングで通知される「Push型」でデータを取得します。
このような「データが追加された」や「データが変更された」といった契機のことをイベントと呼びます。
プログラマは、コードブロックに「受け取ったデータをどのように扱いたいか」を記述しておき、その処理を実行して欲しいイベントに関連付けます。このコードブロックはリスナと呼ばれ、イベントにコードブロックを紐付けることを「リスナを登録する」と表現します。
今回作成するリアルタイムチャットアプリケーションでは、「新着メッセージが到着した」というのがイベントで、「新着メッセージを受け取ったらメッセージ一覧に追加し、画面に新着メッセージを表示する」という処理がリスナです。
イベントが発生するたびにリスナに書いてある処理は実行され、ユーザがわざわざ画面をリロードしなくても「新着メッセージが到着するたびにメッセージを表示する」といったリアルタイムなアプリケーションを開発することがFirebaseでは可能なのです。
Firebaseのイベント一覧
Firebaseでは処理開始の契機として利用できるイベントが全部で5つあります。
イベント | タイミング |
Value イベント | データの新規追加/変更のタイミングで呼び出される |
Child Added イベント | リストのようなデータ構造で、子要素が新規追加されたタイミングで呼び出される |
Child Changed イベント | リストのようなデータ構造で、子要素が変更されたタイミングで呼び出される |
Child Removed イベント | リストのようなデータ構造で、子要素が削除されたタイミングで呼び出される |
Child Moved イベント | リストのようなデータ構造で、子要素が移動されたタイミングで呼び出される |
それぞれについて詳しく解説していきます。
サンプルデータ
今回作成するリアルタイムチャットアプリケーションのために、Webコンソールを使って/messages
に以下のようなデータを用意してください。
Webコンソールの使い方は前回の連載を参考にしてください。
Valueイベント
Valueイベントは、特定のURI以下にデータが新規に追加されたり、変更があった場合に呼び出されます。
今回のサンプルデータの例で言うと、https://<YOUR-FIREBASE-APP>.firebaseio.com/messages
以下に新規メッセージが追加されたり、メッセージが変更された際に処理を実行したい場合、/messages
のValueイベントに対してリスナを登録しておけば良いということになります。
AndroidクライアントでValueイベントに対応するリスナは、ValueEventListener
インタフェースの実装クラスを使って作成します。
データの取得に成功すればonDataChange(DataSnapshot snapshot)
が呼ばれ、何らかの理由で失敗すればonCancelled(FirebaseError error)
が呼ばれます。
さっそくコードで見ていきましょう。
まずはこの通りコードを書いて実行してみてください。ログに以下のように出力されれば成功です。
では、リスト1の各行の意味を順を追って見ていきましょう。
DataSnapshot
リスト1の5行目で、データ取得成功時にonDataChange
の引数として渡されるDataSnapshot
は、文字通り「あるURIのある瞬間のデータのスナップショット」です。つまり、その瞬間Firebaseに存在するデータを意味します。
DataSnapshot
自体はデータの入れ物であり、データそのものを取り出すにはgetValue()
メソッドを使います。FirebaseはスキーマレスなJSONオブジェクトで任意の値を保持できるので、取り出す値はもちろんJSONオブジェクトで表現できる任意の型です。したがって利用者が適宜適切な型にキャストして利用しなければなりません。その時点で指定したURIに何のデータも存在しない場合はnull
が返されます。
また、DataSnapshot
が配列のようなデータ構造(コラム参照)をしている場合、getChildren()
メソッドで各要素をコレクションとして取り出すことができます。
今回のサンプルデータでは、/messages
から取得したDataSnapshot
に対してgetChildren()
すると
これらが順番に取得できます。
さらに、DataSnapshot
がオブジェクトの場合は、child("キー名")
メソッドでキーに対応する値が取得できます。リスト1の7行目でdataSnapshot.child("sender").getValue()
でsender
に対応する値を取り出し、(String)
で文字列型に変換して送信者名を取り出しています。8行目のbody
についても同様です。最後にログ出力して完成です。
型安全なデータの読み出し
リスト1の7、8行目でchild()
メソッドを使って値を取り出した部分は、対応するエンティティクラスを用意することで型安全に取り出すことも可能です。
以下のコードを、
以下のように修正してみてください。
まず、チャット本文を表現するChatMessage
というエンティティクラスを新規に定義します。
次に、リスト1の7、8行目を以下のようにdataSnapshot.getValue(ChatMessage.class)
に書き換えます。
この方がコードの見通しもよく、取り回しもしやすくなったことがわかると思います。
Valueイベントの呼び出しタイミングと注意点
Valueイベントは初回アクセス時に一度呼び出され、対応するURI以下のデータをすべて取得します。それに加え、URI以下のデータが変更される度に毎回呼ばれますが、その都度毎回URI以下の全データを取得し直します。
単純な値を取り出す場合や、データを1回だけ取得できれば良い場合はValueイベントでも問題ありませんが、チャットメッセージ一覧のようなデータ構造の場合、新着メッセージがひとつ届くだけで過去のメッセージも含めてすべてのデータ取り直すことになり、大変非効率です。そういった場合には、次にご紹介するChild Addedイベントを利用すると良いでしょう。
Child Addedイベント
Child Addedイベントは、配列のようなデータ構造に要素が追加されるごとに呼び出されます。
Valueイベントが指定したURI以下の全データを毎回取り直すのに対し、Child Addedイベントは追加された要素だけをDataSnapshot
として受け取るので、チャットメッセージ一覧のようなケースにうってつけです。
Child Addedイベントは、ChildEventListener
インタフェースのonChildAdded(DataSnapshot snapshot, String previousChildKey)
をオーバーライドすることで受信することができます。
onChildAdded
の第一引数のDataSnapshot
は、追加された要素そのものになります。
このように、DataSnapshot
の中身がチャットメッセージの各要素だけになるので分かりやすく、また、新規メッセージが追加されるごとに全データを取り直したりしないので効率的です。
onChildAdded
の第二引数のpreviousKey
は、その要素が全体の何番目かを知るのに利用できるキー(サンプルデータの場合は、"01", "02", "03" などの通し番号)が渡されますが、これの利用方法は後の連載で予定している実践的なテクニックの中でご紹介できればと思います。
最後に、Child Addedイベントにリスナを登録した場合、初回アクセス時に要素数分だけonChildAdded
が呼ばれ、以後新規に要素が追加されるたびに呼ばれます。
Child Changedイベント
Child Changedイベントは、配列のようなデータ構造で、要素に更新があるごとに呼び出されます。
Child Changedイベントは、ChildEventListener
インタフェースのonChildChanged(DataSnapshot snapshot, String previousChildKey)
をオーバーライドすることで受信することができます。
onChildChanged
の第一引数のDataSnapshot
は、更新された要素そのものになります。
ソースコードは、Child Addedのサンプルコードの5行目がonChildChanged(DataSnapshot dataSnapshot, String previousKey)
になる以外はまったく同じです。
実装後、Webコンソールからsender
をJohn
からJack
に変えてみてください。
ログにJack
の情報だけが出力されれば成功です。
Child Removedイベント
Child Removedイベントは、配列のようなデータ構造で、要素が削除されるごとに呼び出されます。
Child Removedイベントは、ChildEventListener
インタフェースのonChildRemoved(DataSnapshot snapshot)
をオーバーライドすることで受信することができます。
onChildRemoved
の引数のDataSnapshot
は、削除された要素そのものになります。
こちらもソースコードは、Child Addedのサンプルコードの5行目がonChildRemoved(DataSnapshot dataSnapshot)
になる以外はまったく同じです。
実装後、Webコンソールからsender
がJack
のチャットメッセージを削除してみてください。
以下のように削除されたJack
のメッセージがログに出力されれば成功です。
Child Movedイベント
Child Movedイベントは、配列のようなデータ構造で、要素が移動されるごとに呼び出されます。
Child Movedイベントは、ChildEventListener
インタフェースのonChildMoved(DataSnapshot snapshot, String previousKey
をオーバーライドすることで受信することができます。
onChildMoved
の第一引数のDataSnapshot
は、移動された要素そのものになります。
こちらもソースコードは、Child Addedのサンプルコードの5行目がonChildMoved(DataSnapshot dataSnapshot, String previousKey)
になる以外はまったく同じです。
Childe Movedイベントは、ソート済みデータの一部に変更があり、順番の並び替えが発生した場合に呼び出されます。データのソートに関しては本連載の後半について解説します。
イベントに関する保証
Firebaseではイベントの通知順について以下の保証があります。
- 必ずデータベースのローカルコピーに最初に状態を反映する
- 一時的にローカルコピーとリモートのデータベースの状態が食い違ったとしても、最終的にきちんと同期される
- ローカルコピーの変更はリモートに書き込まれ、完了後に全クライアントにブロードキャストされる
- Valueイベントは必ず最後に呼び出され、その引数で渡される
DataSnapshot
にはこれまでの変更がすべて含まれていることが保証される
Firebaseではデータベースのローカルコピーをクライアント側に持つため、オフライン状態でも問題なくデータの読み書きができることを連載第1回で述べました。
このため、まずはローカルにデータが書き込まれ、それから中央のデータベースに変更が反映されます。その後、同じデータベースを参照している各クライアントに変更が通知され、最終的にはすべての環境で同じ状態に収束します。
また、先ほどの/messages
の例のように、ひとつのURIに複数のリスナを登録することができますが、その場合でもValueイベントは必ず一番最後に呼び出され、一連の変更がすべて反映されたDataSnapshot
が入っていることが保証されています。あまり同一URIに複数のリスナを登録するケースは多くはないかも知れませんが、頭の片隅に覚えておくと良いかもしれません。
リスナの登録解除
登録したリスナは、ref.removeEventListener(originalListener)
で解除することができます。
もし複数のリスナを登録している場合は、不要になった際に登録した分だけ解除してください。リスナを適切に解除しないと、意図しないイベントコールバックが発生したり、メモリリークにつながる恐れがあります。
一点注意点として、上位のURIでリスナを登録解除したからといって、それ以下のすべてのリスナを解除したということにはなりませんので注意してください。
ワンショットのValueイベントリスナ
一度だけデータを取得するのに使って以後変更があっても利用しないようなケースでは、addListenerForSingleValueEvent()
にValueEventListener
を登録するのが便利です。
以上のようにすると、初回のみ指定したURI以下の全データを取得し、そのままリスナを登録解除してくれます。
データのクエリ
これまでの例では、データを追加した順に取得するのみでしたが、実際には
- データを特定の条件にしたがって並び替える
- データを特定の条件のものだけ取り出す
といった、いわゆる「クエリ」を利用することが複雑なアプリケーション開発には欠かせません。
ここではFirebaseでさまざまなクエリの発行方法をご紹介したいと思います。
Firebaseのクエリで利用できるメソッド一覧
Firebaseのクエリでは、大きく分けて「並び替え」用のメソッドと「条件付き取得」用のメソッドが用意されています。
並び替え
メソッド | 概要 |
orderByChild() | 子要素のキーで並び替え |
orderByKey() | 要素のキーで並び替え |
orderByValue() | 要素の値で並び替え |
orderByPriority() | 要素の優先度で並び替え |
条件付き取得
メソッド | 概要 |
limitToFirst() | 先頭からn件取得 |
limitToLast() | 後方からn件取得 |
startAt() | 条件にマッチする値以降を取得 |
endAt() | 条件にマッチする値以前を取得 |
equalTo() | 条件にマッチする値だけを取得 |
それぞれについて詳しく確認していきたいと思います。
サンプルデータ
さまざまなクエリを試すにあたり、サンプルデータを以下のように変更してください。
また、これに合わせてChatMessage
クラスも以下のように変更してください。
さらに、各ユーザが受信したメールの数を管理するデータも追加します。
ルートノードに以下のようなデータを追加してください。
並び替え
orderByChild()
子要素を特定のキー名でソートする場合は、orderByChild("キー名")
を利用します。まずは以下のコードを参照してください。
これまでと同じように、/messages
の参照ref
を作りますが、そこから更にorderByChild("キー名")
でソートしたいキー名を指定します。上記の例ではtimestamp
を指定しているのでタイムスタンプ順にソートされるはずです。
見事、タイムスタンプでソートされているようです。他にも手元でref.orderByChild("sender")
に変えたりしていろいろ試してみてください。
orderByKey()
子要素を要素自身のキー名でソートする場合は、orderByKey()
を利用します。
サンプルデータの場合は、/messages
以下の "01", "02, "03", "04", "05" がそれぞれの要素のキーになります。
結果は、最初に用意した通りの順番となるはずです。この挙動はオーダリングを指定しなかった場合のデフォルトの挙動です。
orderByValue()
子要素のキーではなく、要素の値でソートしたい場合は、orderByValue()
を利用します。今度は、/counts
に対してリスナを登録してみましょう。以下のようにします。
今回は/counts
に対してChild Addedイベントリスナを登録したので、各子要素は{"Bill" : 7}
といったものが取得できるはずです。
そこでString user = dataSnapshot.getKey()
してユーザ名を取得し、long count = (long) dataSnapshot.getValue();
でメッセージ数を取得しています。
以上のように、count
で昇順にログ出力できたら成功です。
orderByPriority()
もうひとつ、priority
(優先度)で並び替えるためのオプションがあります。これは、各データにユーザが任意の優先度を付けて並び替えをカスタマイズできるように用意されたものです。
実はFirebaseにはデータを降順に並べる手段が標準では用意されていません。したがって降順にするためのワークアラウンドにこのオプションを利用したりします。この辺りは後の連載の実践テクニックでぜひご紹介したいと思います。
条件付き取得
並び替えと組み合わせて、取得数の上限や、どこからどこまで取得するといった範囲を指定することができます。
limitToFirst(), limitToLast()
データを最初から数えていくつまで、最後から数えていくつまで、といった指定をするには、それぞれlimitToFirst()
,limitToLast()
を利用します。
両方試して、それぞれ以下のように最初の2件、最後の2件が取得できることを確認してみてください。
startAt(), endAt()
データの値がどこから始まって、どこまでを含む、といった範囲を指定するには、それぞれstartAt(), endAt()
を利用します。 単独で使うことも、コンビネーションで使うこともできます。
上記の例ですと、3~17の範囲の要素が取得できるので、以下のようにログに出力されれば成功です。
ここにさらにref.orderByValue().startAt(3).endAt(17).limitToFirst(1)
のようにして範囲内の最初の1件に絞るといったことも容易です。
equalTo()
最後に、指定した値とぴったり同じ値でマッチングするにはequalTo()
を利用します。今度は/messages
からsender
がMike
のメッセージを取得してみましょう。
以下のように"Mike"のメッセージだけが取得できていれば成功です。
まとめ
いかがだったでしょうか。
今回の連載では、同じようにデータを取得する場合でも、一気に全データを取得したり、特定のケースだけに特化した効率的なやり方があることを見ていきました。また、クエリを利用すればデータを並び替えたり取得条件を指定して柔軟にデータを取り出すことができることも確認しました。
次回の連載では、このチャットアプリケーションに新規にデータを追加する方法を解説し、ケースにあったさまざまなデータの保存方法をご紹介したいと思います。
どうぞお楽しみに。