前回の連載では、
今回は例として、
/messages以下に、チャットのメッセージ情報を {"sender":"送信者", "body":"本文"}の一覧形式で格納/counts以下に、どのユーザが何通メッセージを受信したかという情報を {"ユーザ名":数}という形式で格納
イベントとリスナ
前回の
このような
プログラマは、
今回作成するリアルタイムチャットアプリケーションでは、
イベントが発生するたびにリスナに書いてある処理は実行され、
Firebaseのイベント一覧
Firebaseでは処理開始の契機として利用できるイベントが全部で5つあります。
| イベント | タイミング | 
|---|---|
| Value イベント | データの新規追加/ | 
| Child Added イベント | リストのようなデータ構造で、 | 
| Child Changed イベント | リストのようなデータ構造で、 | 
| Child Removed イベント | リストのようなデータ構造で、 | 
| Child Moved イベント | リストのようなデータ構造で、 | 
それぞれについて詳しく解説していきます。
サンプルデータ
今回作成するリアルタイムチャットアプリケーションのために、/messagesに以下のようなデータを用意してください。
{
  "messages" : {
    "01" : {
      "body" : "Hi, there!",
      "sender" : "John"
    },
    "02" : {
      "body" : "What's up?",
      "sender" : "Steve"
    },
    "03" : {
      "body" : "hey hey hey!",
      "sender" : "Bill"
    }
  }
}
Webコンソールの使い方は前回の連載を参考にしてください。
Valueイベント
Valueイベントは、
今回のサンプルデータの例で言うと、https://<YOUR-FIREBASE-APP>.firebaseio.以下に新規メッセージが追加されたり、/messagesのValueイベントに対してリスナを登録しておけば良いということになります。
AndroidクライアントでValueイベントに対応するリスナは、ValueEventListenerインタフェースの実装クラスを使って作成します。
データの取得に成功すればonDataChange(DataSnapshot snapshot)が呼ばれ、onCancelled(FirebaseError error)が呼ばれます。
さっそくコードで見ていきましょう。
Firebase ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/messages");
ref.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot snapshot) {
        for (DataSnapshot dataSnapshot : snapshot.getChildren()) {
            String sender = (String) dataSnapshot.child("sender").getValue();
            String body = (String) dataSnapshot.child("body").getValue();
            Log.d("Firebase", String.format("sender:%s, body:%s", sender, body));
        }
    }
    @Override
    public void onCancelled(FirebaseError error) {
    }
});
まずはこの通りコードを書いて実行してみてください。ログに以下のように出力されれば成功です。
D/Firebase: sender:John, body:Hi, there!
D/Firebase: sender:Steve, body:What's up?
D/Firebase: sender:Bill, body:hey hey hey!
では、
DataSnapshot
リスト1の5行目で、onDataChangeの引数として渡されるDataSnapshotは、
DataSnapshot自体はデータの入れ物であり、getValue()メソッドを使います。FirebaseはスキーマレスなJSONオブジェクトで任意の値を保持できるので、nullが返されます。
また、DataSnapshotが配列のようなデータ構造getChildren()メソッドで各要素をコレクションとして取り出すことができます。
今回のサンプルデータでは、/messagesから取得したDataSnapshotに対してgetChildren()すると
{
  "body" : "Hi, there!",
  "sender" : "John"
},
{
  "body" : "What's up?",
  "sender" : "Steve"
},
{
  "body" : "hey hey hey!",
  "sender" : "Bill"
}
これらが順番に取得できます。
さらに、DataSnapshotがオブジェクトの場合は、child("キー名")メソッドでキーに対応する値が取得できます。リスト1の7行目でdataSnapshot.でsenderに対応する値を取り出し、(String)で文字列型に変換して送信者名を取り出しています。8行目のbodyについても同様です。最後にログ出力して完成です。
型安全なデータの読み出し
リスト1の7、child()メソッドを使って値を取り出した部分は、
以下のコードを、
String sender = (String) dataSnapshot.child("sender").getValue();
String body = (String) dataSnapshot.child("body").getValue();
以下のように修正してみてください。
まず、ChatMessageというエンティティクラスを新規に定義します。
public class ChatMessage {
    public String body;
    public String sender;
}
次に、dataSnapshot.に書き換えます。
ChatMessage chatMessage = dataSnapshot.getValue(ChatMessage.class);
String sender = chatMessage.sender;
String body = chatMessage.body;
この方がコードの見通しもよく、
Valueイベントの呼び出しタイミングと注意点
Valueイベントは初回アクセス時に一度呼び出され、
単純な値を取り出す場合や、
Child Addedイベント
Child Addedイベントは、
Valueイベントが指定したURI以下の全データを毎回取り直すのに対し、DataSnapshotとして受け取るので、
Child Addedイベントは、ChildEventListenerインタフェースのonChildAdded(DataSnapshot snapshot, String previousChildKey)をオーバーライドすることで受信することができます。
onChildAddedの第一引数のDataSnapshotは、
Firebase ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/messages");
ref.addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String previousKey) {
        ChatMessage chatMessage = dataSnapshot.getValue(ChatMessage.class);
        String sender = chatMessage.sender;
        String body = chatMessage.body;
        Log.d("Firebase", String.format("onChildAdded, sender:%s, body:%s", sender, body));
    }
    ...
});
このように、DataSnapshotの中身がチャットメッセージの各要素だけになるので分かりやすく、
onChildAddedの第二引数のpreviousKeyは、
最後に、onChildAddedが呼ばれ、
Child Changedイベント
Child Changedイベントは、
Child Changedイベントは、ChildEventListenerインタフェースのonChildChanged(DataSnapshot snapshot, String previousChildKey)をオーバーライドすることで受信することができます。
onChildChangedの第一引数のDataSnapshotは、
ソースコードは、onChildChanged(DataSnapshot dataSnapshot, String previousKey)になる以外はまったく同じです。
実装後、senderをJohnからJackに変えてみてください。
ログにJackの情報だけが出力されれば成功です。
D/Firebase: onChildChanged, sender:Jack, body:Hi, there!
Child Removedイベント
Child Removedイベントは、
Child Removedイベントは、ChildEventListenerインタフェースのonChildRemoved(DataSnapshot snapshot)をオーバーライドすることで受信することができます。
onChildRemovedの引数のDataSnapshotは、
こちらもソースコードは、onChildRemoved(DataSnapshot dataSnapshot)になる以外はまったく同じです。
実装後、senderがJackのチャットメッセージを削除してみてください。
以下のように削除されたJackのメッセージがログに出力されれば成功です。
D/Firebase: onChildRemoved, sender:Jack, body:Hi, there!
Child Movedイベント
Child Movedイベントは、
Child Movedイベントは、ChildEventListenerインタフェースのonChildMoved(DataSnapshot snapshot, String previousKeyをオーバーライドすることで受信することができます。
onChildMovedの第一引数のDataSnapshotは、
こちらもソースコードは、onChildMoved(DataSnapshot dataSnapshot, String previousKey)になる以外はまったく同じです。
Childe Movedイベントは、
イベントに関する保証
Firebaseではイベントの通知順について以下の保証があります。
- 必ずデータベースのローカルコピーに最初に状態を反映する
 - 一時的にローカルコピーとリモートのデータベースの状態が食い違ったとしても、
最終的にきちんと同期される  - ローカルコピーの変更はリモートに書き込まれ、
完了後に全クライアントにブロードキャストされる  - Valueイベントは必ず最後に呼び出され、
その引数で渡される DataSnapshotにはこれまでの変更がすべて含まれていることが保証される 
Firebaseではデータベースのローカルコピーをクライアント側に持つため、
このため、
また、/messagesの例のように、DataSnapshotが入っていることが保証されています。あまり同一URIに複数のリスナを登録するケースは多くはないかも知れませんが、
リスナの登録解除
登録したリスナは、ref.で解除することができます。
もし複数のリスナを登録している場合は、
一点注意点として、
ワンショットのValueイベントリスナ
一度だけデータを取得するのに使って以後変更があっても利用しないようなケースでは、addListenerForSingleValueEvent()にValueEventListenerを登録するのが便利です。
ref.addListenerForSingleValueEvent(new ValueEventListener() {
    @Override
    public void onDataChange(DataSnapshot snapshot) {
        // do some stuff once
    }
    @Override
    public void onCancelled(FirebaseError firebaseError) {
    }
});
以上のようにすると、
データのクエリ
これまでの例では、
- データを特定の条件にしたがって並び替える
 - データを特定の条件のものだけ取り出す
 
といった、
ここではFirebaseでさまざまなクエリの発行方法をご紹介したいと思います。
Firebaseのクエリで利用できるメソッド一覧
Firebaseのクエリでは、
並び替え
| メソッド | 概要 | 
|---|---|
orderByChild() | 子要素のキーで並び替え | 
orderByKey() | 要素のキーで並び替え | 
orderByValue() | 要素の値で並び替え | 
orderByPriority() | 要素の優先度で並び替え | 
条件付き取得
| メソッド | 概要 | 
|---|---|
limitToFirst() | 先頭からn件取得 | 
limitToLast() | 後方からn件取得 | 
startAt() | 条件にマッチする値以降を取得 | 
endAt() | 条件にマッチする値以前を取得 | 
equalTo() | 条件にマッチする値だけを取得 | 
それぞれについて詳しく確認していきたいと思います。
サンプルデータ
さまざまなクエリを試すにあたり、
{
  "messages" : {
    "01" : {
      "body" : "Hi, there!",
      "sender" : "John",
      "timestamp" : 20160123
    },
    "02" : {
      "body" : "What's up?",
      "sender" : "Steve",
      "timestamp" : 20151004
    },
    "03" : {
      "body" : "hey hey hey!",
      "sender" : "Bill",
      "timestamp" : 20151217
    },
    "04" : {
      "body" : "ho ho ho:)",
      "sender" : "Mike",
      "timestamp" : 20160403
    },
    "05" : {
      "body" : "howdy",
      "sender" : "Clint",
      "timestamp" : 20140411
    }
  }
}
また、ChatMessageクラスも以下のように変更してください。
public class ChatMessage {
    public String body;
    public String sender;
    public long timestamp;
}
さらに、
ルートノードに以下のようなデータを追加してください。
{
  "counts" : {
    "john" : 16,
    "Steve" : 2,
    "Bill" : 7,
    "Mike" : 25,
    "Clint" : 9
  }
}
並び替え
orderByChild()
子要素を特定のキー名でソートする場合は、orderByChild("キー名")を利用します。まずは以下のコードを参照してください。
Firebase ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/messages");
Query query = ref.orderByChild("timestamp");
query.addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String previousKey) {
        ChatMessage chatMessage = dataSnapshot.getValue(ChatMessage.class);
        String sender = chatMessage.sender;
        String body = chatMessage.body;
        long timestamp = chatMessage.timestamp;
        Log.d("Firebase", String.format("onChildAdded, sender:%s, body:%s, timestamp:%d", sender, body, timestamp));
    }
これまでと同じように、/messagesの参照refを作りますが、orderByChild("キー名")でソートしたいキー名を指定します。上記の例ではtimestampを指定しているのでタイムスタンプ順にソートされるはずです。
D/Firebase: onChildAdded, sender:Clint, body:howdy, timestamp:20140411
D/Firebase: onChildAdded, sender:Steve, body:What's up?, timestamp:20151004
D/Firebase: onChildAdded, sender:Bill, body:hey hey hey!, timestamp:20151217
D/Firebase: onChildAdded, sender:John, body:Hi, there!, timestamp:20160123
D/Firebase: onChildAdded, sender:Mike, body:ho ho ho:), timestamp:20160403
見事、ref.に変えたりしていろいろ試してみてください。
orderByKey()
子要素を要素自身のキー名でソートする場合は、orderByKey()を利用します。
サンプルデータの場合は、/messages以下の "01", "02, "03", "04", "05" がそれぞれの要素のキーになります。
Firebase ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/messages");
Query query = ref.orderByKey();
query.addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String previousKey) {
        ChatMessage chatMessage = dataSnapshot.getValue(ChatMessage.class);
        String sender = chatMessage.sender;
        String body = chatMessage.body;
        long timestamp = chatMessage.timestamp;
        Log.d("Firebase", String.format("onChildAdded, sender:%s, body:%s, timestamp:%d", sender, body, timestamp));
    }
結果は、
orderByValue()
子要素のキーではなく、orderByValue()を利用します。今度は、/countsに対してリスナを登録してみましょう。以下のようにします。
Firebase ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/counts");
Query query = ref.orderByValue();
query.addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String previousKey) {
        String user = dataSnapshot.getKey();
        long count = (long) dataSnapshot.getValue();
        Log.d("Firebase", String.format("user: %s's count=%d", user, count));
    }
今回は/countsに対してChild Addedイベントリスナを登録したので、{"Bill" : 7}といったものが取得できるはずです。
そこでString user = dataSnapshot.してユーザ名を取得し、long count = (long) dataSnapshot.でメッセージ数を取得しています。
D/Firebase: user: Steve's count=2
D/Firebase: user: Bill's count=7
D/Firebase: user: Clint's count=9
D/Firebase: user: john's count=16
D/Firebase: user: Mike's count=25
以上のように、countで昇順にログ出力できたら成功です。
orderByPriority()
もうひとつ、priority
実はFirebaseにはデータを降順に並べる手段が標準では用意されていません。したがって降順にするためのワークアラウンドにこのオプションを利用したりします。この辺りは後の連載の実践テクニックでぜひご紹介したいと思います。
条件付き取得
並び替えと組み合わせて、
limitToFirst(), limitToLast()
データを最初から数えていくつまで、limitToFirst(),limitToLast()を利用します。
Firebase ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/counts");
Query query = ref.orderByValue().limitToFirst(2);
// or
Query query = ref.orderByValue().limitToLast(2);
query.addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String previousKey) {
        String user = dataSnapshot.getKey();
        long count = (long) dataSnapshot.getValue();
        Log.d("Firebase", String.format("user: %s's count=%d", user, count));
    }
両方試して、
D/Firebase: user: Steve's count=2
D/Firebase: user: Bill's count=7
// or
D/Firebase: user: john's count=16
D/Firebase: user: Mike's count=25
startAt(), endAt()
データの値がどこから始まって、startAt(), endAt()を利用します。 単独で使うことも、
Firebase ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/counts");
Query query = ref.orderByValue().startAt(3).endAt(17);
query.addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String previousKey) {
        String user = dataSnapshot.getKey();
        long count = (long) dataSnapshot.getValue();
        Log.d("Firebase", String.format("user: %s's count=%d", user, count));
    }
上記の例ですと、
D/Firebase: user: Bill's count=7
D/Firebase: user: Clint's count=9
D/Firebase: user: john's count=16
ここにさらにref.のようにして範囲内の最初の1件に絞るといったことも容易です。
equalTo()
最後に、equalTo()を利用します。今度は/messagesからsenderがMikeのメッセージを取得してみましょう。
Firebase ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com/messages");
Query query = ref.orderByChild("sender").equalTo("Mike");
query.addChildEventListener(new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String previousKey) {
        ChatMessage chatMessage = dataSnapshot.getValue(ChatMessage.class);
        String sender = chatMessage.sender;
        String body = chatMessage.body;
        long timestamp = chatMessage.timestamp;
        Log.d("Firebase", String.format("onChildAdded, sender:%s, body:%s, timestamp:%d", sender, body, timestamp));
    }
D/Firebase: onChildAdded, sender:Mike, body:ho ho ho:), timestamp:20160403
まとめ
いかがだったでしょうか。
今回の連載では、
次回の連載では、
どうぞお楽しみに。
