Firebase Realtime Databaseを使う手順 〜データ設計からセキュリティ、組み込みまで〜

最近FirebaseのRealtimeDatabaseを触っているので、手順やポイントをまとめてみた。

# Realtime Databaseとは
[Firebase Realtime Datbase](https://firebase.google.com/docs/database/?hl=ja)とは、Googleが提供するNoSQLデータベースサービスであり、接続された全てのアプリ・Web等でデータの変更がリアルタイムに反映されるもの。
即時性が必要なチャットアプリとかに最適。

# 導入
## Firebaseのアカウント作成
Firebaseにアクセスして手順通りにステップを踏むだけでOK。
あとは左ペインの"Database"に行くとGUIでKey:Valueを追加していくことが出来る。

## データ構造の設計
[このページ](https://firebase.google.com/docs/database/android/structure-data?hl=ja)を詳しく読めば大体の事が書いているので、設計に入る前に一読した方がよさそう。

RDBとは使われ方も性質も全くことなり、非正規化を意識して出来るだけフラットなデータ構造にする必要がある。
例えば、以下のような場合を考えてみる。(極端な例)
ユーザーのグループか何かがあって、グループに属するメンバー/年齢/自己紹介文があり、Bobの自己紹介文だけがすごい長い。
アプリでは、ユーザー一覧画面として名前のみをリストで見せたい要件があった場合、以下のような構造だと必要のない年齢や長過ぎるBobの自己紹介文までFetchしてしまう。(/something/groups/group_a/users でアクセスすると、子ノード含む全ての情報をFetch)


```json:良くない例
{
  "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が返れば属していないと判断出来る。

また、フラット化されていない例だと、どのグループに属しているかを判断するためには、すべてのデータを舐めないといけない。

```json:フラット化した例
{
  "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の依存関係を記述する。

```build.gradle
compile 'com.google.firebase:firebase-database:10.0.1'
compile 'com.google.firebase:firebase-auth:10.0.1' // <- 認証使わない場合はなくてもOK（後述）
```

### 基本的な使い方

以下のような構造のデータを取得することを想定する。

```json:サンプル
{
  "root": {
    "something": "foo"
  }
}
```

```java:読み出し
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) {
    }
});
```

```java:書き込み
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference myRef = database.getReference("something");
myRef.setValue("poge!");
```

オブジェクトになっている場合はクラスを作る。

```json
{
  "root": {
    "something": {
      "foo": "bar",
      "hoge": "fuga"
    }
  }
}
```

```java:同じ構造のクラス作成
public class Somethig {
    private String foo;
    pirvate String hoge;
    // getter / setterは省略
}
```

```java:読み出し
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```というものを使うとその部分だけの変更を検知することが出来る。

```json:子ノード付き
{
  "root": {
    "something": {
      "some_child": {
        "foo": "bar"
      },
      "hoge": {
        "puge": "page"
      }
    }
  }
}
```

```java:子ノードだけ
// 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``` はここで使うもの。

```java:匿名認証してからアクセス
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を使うべきかどうかも合わせて検討した方がよい。