最近FirebaseのRealtimeDatabaseを触っているので、手順やポイントをまとめてみた。
Realtime Databaseとは
Firebase Realtime Datbaseとは、Googleが提供するNoSQLデータベースサービスであり、接続された全てのアプリ・Web等でデータの変更がリアルタイムに反映されるもの。
即時性が必要なチャットアプリとかに最適。
導入
Firebaseのアカウント作成
Firebaseにアクセスして手順通りにステップを踏むだけでOK。
あとは左ペインの"Database"に行くとGUIでKey:Valueを追加していくことが出来る。
データ構造の設計
このページを詳しく読めば大体の事が書いているので、設計に入る前に一読した方がよさそう。
RDBとは使われ方も性質も全くことなり、非正規化を意識して出来るだけフラットなデータ構造にする必要がある。
例えば、以下のような場合を考えてみる。(極端な例)
ユーザーのグループか何かがあって、グループに属するメンバー/年齢/自己紹介文があり、Bobの自己紹介文だけがすごい長い。
アプリでは、ユーザー一覧画面として名前のみをリストで見せたい要件があった場合、以下のような構造だと必要のない年齢や長過ぎるBobの自己紹介文までFetchしてしまう。(/something/groups/group_a/users でアクセスすると、子ノード含む全ての情報をFetch)
{
"something": {
"groups": {
"group_a": {
"users": {
"Alice": {
"age": "25",
"introduction": "Hello, this is Alice."
},
"Bob": {
"age": "50",
"introduction": "Hello, this is Bob. I'm from California and blah blah........長い文章"
}
}
},
"group_b": {
"users": {
"Steve": {
"age": "20",
"introduction": "Hello, this is Steve."
},
"Joey": {
"age": "28",
"introduction": "Hello, this is Joey."
}
}
}
}
}
}
これを、出来るだけフラットにしてみる。
こうすることで、/something/groups/group_a にアクセスすると、属するメンバーの名前だけが取得され無駄なBobの自己紹介文は取得しなくて済む。
group <-> userの間で双方向に参照を持たせることで、以下のようなことも実現可能になる。
- 自分が属するグループのメンバーを取得する
- 自分がどのグループに属しているかを取得する
- Steveがgroup_aに属しているかを判断したい場合、/something/group_a/Steve でnullが返れば属していないと判断出来る。
また、フラット化されていない例だと、どのグループに属しているかを判断するためには、すべてのデータを舐めないといけない。
{
"something": {
"groups": {
"group_a": {
"Alice": true,
"Bob": true
},
"group_b": {
"Steve": true,
"Joey": true
}
},
"user_list": {
"Alice": {
"groups": {
"group_a": true
}
},
"Bob": {
"groups": {
"group_a": true
}
},
"Steve": {
"groups": {
"group_b": true
}
},
"Joey": {
"groups": {
"group_b": true
}
}
},
"user_info": {
"Alice": {
"age": "25",
"introduction": "Hello, this is Alice."
},
"Bob": {
"age": "50",
"introduction": "Hello, this is Bob. I'm from California and blah blah........長い文章"
},
"Steve": {
"age": "20",
"introduction": "Hello, this is Steve."
},
"Joey": {
"age": "20",
"introduction": "Hello, this is Joey."
}
}
}
}
データのデプロイ
データ構造が決定したら、Firebase Reatime Databaseのページにアクセスして、作ったjsonをそのままインポートすることも可能。
JavaやNode.jsのサーバー用SDKも公開されているので、自作ツールを作ることもできる。(別の投稿で記載予定)
アプリで使う(Androidの例)
アプリでの使い方は非常に単純。まずはgradleにrealtime databaseの依存関係を記述する。
compile 'com.google.firebase:firebase-database:10.0.1'
compile 'com.google.firebase:firebase-auth:10.0.1' // <- 認証使わない場合はなくてもOK(後述)
基本的な使い方
以下のような構造のデータを取得することを想定する。
{
"root": {
"something": "foo"
}
}
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference myRef = database.getReference("something");
myRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(final DataSnapshot dataSnapshot) {
String foo = dataSnapshot.getValue(String.class)
// foo => "foo"
}
@Override
public void onCancelled(final DatabaseError databaseError) {
}
});
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference myRef = database.getReference("something");
myRef.setValue("poge!");
オブジェクトになっている場合はクラスを作る。
{
"root": {
"something": {
"foo": "bar",
"hoge": "fuga"
}
}
}
public class Somethig {
private String foo;
pirvate String hoge;
// getter / setterは省略
}
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference myRef = database.getReference("something");
myRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(final DataSnapshot dataSnapshot) {
Something something = dataSnapshot.getValue(Something.class)
String foo = something.getFoo();
String hoge = something.getHoge();
// foo => "bar"
// hoge => "fuga
}
@Override
public void onCancelled(final DatabaseError databaseError) {
}
});
onDataChange
は初回のアタッチ時と、あとはFirebaseのコンソール上のJSONを変更すると瞬時にアプリ側に通知される。
ただ、ここをListenしていると、子ノード含む全てのデータがfetchされるらしい。
データが大量にある場合は、転送量が多くなるため要注意。次の用にListenするノードを指定したりして最適化すると良いと思われる。
必要な子ノードだけListenする
ValueEventListener
ではなく ChildEventListener
というものを使うとその部分だけの変更を検知することが出来る。
{
"root": {
"something": {
"some_child": {
"foo": "bar"
},
"hoge": {
"puge": "page"
}
}
}
}
// some_childの変更だけをListenし、上記の"puge"の変更は検知しない。
DatabaseReference childRef = database.getReference("some_child");
childRef.addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(final DataSnapshot dataSnapshot, final String s) {
}
@Override
public void onChildChanged(final DataSnapshot dataSnapshot, final String s) {
}
@Override
public void onChildRemoved(final DataSnapshot dataSnapshot) {
}
@Override
public void onChildMoved(final DataSnapshot dataSnapshot, final String s) {
}
@Override
public void onCancelled(final DatabaseError databaseError) {
}
});
セキュリティ
セキュリティを設定しないと意図しないユーザーからデータが取得されたり、書き込みがあったりなど想定外の事態を招く可能性が高い。
その為、しっかりとしたセキュリティ設計が大事。
FirebaseのRealtime Databaseコンソールの「ルール」タブから確認が可能。
// 全部のノードに対して書き込みも読み出しも可能(やらないほうが良い)
{
"rules": {
".read": true,
".write": true
}
}
// 全部のノードに対して読み出しのみ可能(やらない方が良い)
{
"rules": {
".read": true,
".write": false
}
}
// /something以下に対して読み出し可能、書き込み不可
// /somethingと同レベルの他のノードは読み書き不可
{
"rules": {
".read": false,
".write": false,
"something": {
".read": true,
".write": false
}
}
}
また、変数も組み込まれており、以下のように指定することも可能。
// uidが一致した場合のみ書き込み可能
{
"rules": {
"users": {
"$uid": {
".write": "$uid === auth.uid"
}
}
}
}
// 書き込みはアドミンの場合のみ可能
// NodeのSDKとかで管理側のみが書き込める場合なんかに有効
{
"rules": {
"something": {
".read": true,
".write": "auth != null && auth.isAdmin == true"
}
}
}
// 認証されたユーザのみ読み出し可能
// 基本的に全ユーザー読み出し可能だが、publicに公開したくない場合に有効
// この後にアプリで認証する場合を記載
{
"rules": {
"something": {
".read": "auth != null",
".write": "auth != null && auth.isAdmin == true"
}
}
}
アプリで匿名認証と組み合わせてRealtime Databaseにアクセスする
セキュリティに一番最後のruleを適用した場合に、アプリ側で認証を使いアクセスを行わなければならない。
ここではFirebase Authenticationの匿名認証を用いて試してみる。
※最初の方のgradleに記載した firebase-auth
はここで使うもの。
mAuth = FirebaseAuth.getInstance();
mAuth.signInAnonymously()
.addOnCompleteListener(new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull final Task<AuthResult> task) {
// anonymous login complete.
// ここでRealtime Databaseの処理を行う。
// auth payloadがRealtime Databaseに渡されて認証される。
// Firebase Authenticationの画面でUIDなどの一覧が見える。
// ここでは匿名認証だが、他にもtwitterやfacebookなども使うことが出来るらしい
readData();
}
});
public void readData() {
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference myRef = database.getReference("something");
myRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(final DataSnapshot dataSnapshot) {
// 続きの処理
}
@Override
public void onCancelled(final DatabaseError databaseError) {
}
});
}
一通り使ってみて
最初の設計さえ念入りに行えば、バックエンドの処理は全てFirebaseが行ってくれる。
リアルタイム性が必要とされるサービスで組み合わせるとかなり強力なツールとなりそう。
ただ、やはりNoSQLについてはリレーションが複雑なデータ構造には不向きなようなので、RDBを使うべきかどうかも合わせて検討した方がよい。