これまでの連載で見てきたように、
しかしながら、
今回は例として以下のようなチャットアプリケーションを想定し、
- チャットルームが複数存在する
- チャットのメッセージはルームごとに管理する
- 複数ユーザがおり、
ユーザは任意のルームに参加して発言することができる
効率的なデータ構造
データのネストを避ける
FirebaseはJSONのツリーを最大で32階層まで持つことができます。これは一般的なアプリケーションを作るのに十分な深さです。
今回のアプリケーションでは、
{
"chats": {
"room01": {
"title": "チャットルーム01",
"messages": {
"message01": {
"sender": "Fumihiko Shiroyama",
"message": "こんにちは。誰かいますか?"
},
"message02": { ... },
"message03": { ... }
}
},
"room02": { ... }
}
}
まず/chats
という階層を作り、/chats/
、/chats/
のように存在しています。
チャットルームの下には/chats/$room_
といった形でルームのタイトルが保存され、/chats/$room_
の下には/chats/$room_
の形式でルームのメッセージ一覧が保持されており、sender
、message
でそれぞれ送信者とメッセージ本文が保存されています。
一見すると何の問題もなさそうなデータ構造ですが、
/chats
にValueEventListener
をセットしてDataSnapshot#getChildren()
メソッドを使ってループを回しながら取得する方法などが考えられますが、
Firebaseはあるパスに存在するデータを取得する際に、
できるだけ階層を浅くする
このため、ユーザ一覧
、ルーム一覧
、本文一覧
でツリーを分けることにしてみましょう。
{
"users": {
"shiroyama": { "name": "Fumihiko Shiroyama" },
"tanaka": { ... },
"sato": { ... }
},
"rooms": {
"room01": {
"title": "チャットルーム01",
"members": {
"member01": "shiroyama",
"member02": "tanaka"
}
},
"room02": { ... }
},
"messages": {
"room01": {
"message01": {
"sender": "shiroyama",
"message": "こんにちは。誰かいますか?"
},
"message02": {
"sender": "tanaka",
"message": "はい、いますよ。"
},
"message03": { ... }
},
"room02": { ... }
}
}
これでずいぶん良くなりました。
ユーザ一覧が/users
へ、/rooms
へ、/messages
へ分かれたので、
ところがこれでもまだ問題があります。もしユーザshiroyama
が自分の参加しているルーム一覧を取得したくなったとしたら、/rooms
以下の全ルームをひとつひとつ走査しながら自分がそのルームのメンバーかどうかを確認する必要があります。もしルームが何千と存在すれば、
セキュリティルールはフィルタとしては利用できない
上記の例が良くない理由がもうひとつあります。
前回、
Firebaseでは、
今回の場合、/rooms/$room_
以下へのアクセスは、member01
だけ成功してmember02
は失敗するのではなく、
{
"rules": {
"rooms": {
"$room_id": {
"members": {
"member01": {
".read": true
},
"member02": {
".read": false
}
}
}
}
}
}
これは公式ドキュメントにも説明されている仕様通りの挙動なので注意してください。
したがって、
双方向のリレーションを作成する
これらの状況を踏まえて、
{
"users": {
"shiroyama": {
"name": "Fumihiko Shiroyama",
"rooms": {
"room01": true,
"room02": true
}
},
"tanaka": { ... },
"sato": { ... }
},
"rooms": {
"room01": {
"title": "チャットルーム01",
"members": {
"shiroyama": true,
"tanaka": true
}
},
"room02": { ... }
},
"messages": {
"room01": {
"message01": {
"sender": "shiroyama",
"message": "こんにちは。誰かいますか?"
},
"message02": {
"sender": "tanaka",
"message": "はい、いますよ。"
},
"message03": { ... }
},
"room02": { ... }
}
}
/users/$user_
以下に、/rooms/$room_
にもユーザ一覧を保持するようになりました。
こうすることで、
値はいずれもtrue
にしていますが、
これは、
残念ながら、/users
の下にも/rooms
の下にもいわば重複するデータが存在することになります
しかしながら、
データを結合する
さて、
リレーショナルデータベースでは、
結合から言うと、
まずは以下の例を見てください。
final Firebase messageRef = ref.child("messages");
final Firebase userRef = ref.child("users");
messageRef.child("room01").addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
for (DataSnapshot snapshot : dataSnapshot.getChildren()) {
final Message message = snapshot.getValue(Message.class);
userRef.child(message.sender).addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
Log.d(TAG, "message: " + message.message);
Log.d(TAG, "sender: " + message.sender);
User user = dataSnapshot.getValue(User.class);
Log.d(TAG, "sender's full name: " + user.name);
}
@Override
public void onCancelled(FirebaseError firebaseError) {
Log.e(TAG, firebaseError.getMessage(), firebaseError.toException());
}
});
}
}
@Override
public void onCancelled(FirebaseError firebaseError) {
Log.e(TAG, firebaseError.getMessage(), firebaseError.toException());
}
});
1~2行目で/messages
と/users
のリファレンスをそれぞれ取得し、/messages/
に対してValueEventListener
をセットして6~7行目でメッセージ一覧を取得しています。ここまでは第3回の内容を参考にしていただければ問題ないと思います。
次に、/messages/
のループの中でuserRef.
に対して更にValueEventListener
をセットして、
リレーショナルデータベースに慣れていると、n+1問題
でパフォーマンスが心配になるかもしれませんが、
結局のところ、
なお、
結果的に、
データにIndexを作成する
連載第3回の.indexOn
ルールを使ってデータにIndexを作成することで、
たとえば、
/rooms/$room_
以下にtimestamp
情報を追加しましょう。
{
"rooms": {
"room01": {
"title": "チャットルーム01",
"members": {
"shiroyama": true,
"tanaka": true
},
"timestamp": 1464511789
},
"room02": { ... }
}
}
このtimestamp
のIndexを作成するには、
{
"rules": {
"rooms": {
"$room_id": {
".indexOn": ["timestamp"]
}
}
}
}
こうすることでtimestamp
にIndexが作成され、
読み出しのコードは以下のようになります。
Firebase roomRef = ref.child("rooms");
roomRef.orderByChild("timestamp").addListenerForSingleValueEvent(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
for (DataSnapshot snapshot : dataSnapshot.getChildren()) {
Room room = snapshot.getValue(Room.class);
Log.d(TAG, "title: " + room.title);
Log.d(TAG, "timestamp: " + room.timestamp);
}
}
@Override
public void onCancelled(FirebaseError firebaseError) {
Log.e(TAG, firebaseError.getMessage(), firebaseError.toException());
}
});
/rooms
へのリファレンスを作成し、orderByChild("timestamp")
で並び替えているだけです。こちらも、timestamp
の昇順
なお、".indexOn": ["timestamp", "title"]
のようにカンマ区切りで追加するだけです。
デフォルトで作成されるIndex
Indexは、
今回の例では/rooms/$room_
の部分はキーなので、
プライオリティ(優先度)
第3回で少し触れましたが、
Firebaseではデータ作成時に、
今回の例では、timestamp
を符号反転させたものを設定してみましょう。こうすることでtimestamp
を降順に並べることが可能です。
Firebase roomRef = ref.child("rooms");
roomRef.child("room01").setValue(room01);
roomRef.child("room01").setPriority(-room01.timestamp);
詳細は割愛しますが、room01
の情報を保存するロジックで、setPriority()
してtimestamp
の符号をマイナスにしたものをセットしているだけです。
読み出すコードは以下のとおりです。
Firebase roomRef = ref.child("rooms");
roomRef.orderByPriority().addListenerForSingleValueEvent(new ValueEventListener() { // 略 }
ValueEventListener
の中身は昇順読み出しとまったく同様です。ルーム一覧が降順に並べば成功です。
まとめ
いかがだったでしょうか。
今回はFirebaseのデータベース構造を効率的に設計し、
今回までで、
さて、