既存のDBをFirebase Realtime DBへマイグレーション時に考えたこと

# Firebase Realtime DBでマイグレーション
Firebase Realtime DBを利用するにあたって大きな障壁になるマイグレーションについて考えてみました。特に既存のDBからFirebaseへマイグレーションを行う際の方法について言及していこうと思います。

## マイグレーションの2つのストラテジー
既存のDBからFirebase Realtime DBへのマイグレーションは以下の2つが提案されています。

1. Background Sync
2. Double Write

### Background Sync
このストラテジーは、既存DBとFirebaseをサーバー間で同期するストラテジーです。

<img width="1101" alt="スクリーンショット 2016-12-09 10.10.34.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/c6c28cc1-914e-2299-e4df-d127f3c8c644.png">

このストラテジーの特徴は、全てをデータの移行をサーバーが行うことです。既存DBがFirebaseにレプリケーションされます。データ量の多いサービスは、現実的ではないかもしれません。

### Double Write
このストラテジーは、クライアントとサーバーを利用します。

<img width="826" alt="スクリーンショット 2016-12-09 10.16.32.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/5e3dc6a8-f24e-16d1-16a0-c765d93f01d1.png">

このストラテジーの特徴は、データ移行のためにかかる負荷をクライアントに分散できることです。しかし、クライアントが起動されないとマイグレーションを行われないため、マイグレーション完了までには時間がかかるかもしれません。
さらに、クライアントだけでは、全てを移行することができないため、最終的には、サーバー間で移行を行う必要があります。

## DBのマイグレーションはクライアントで行う
DBのマイグレーションはサーバーで行うのが一般的だと思います。
サーバーではなくクライアントで行う方法は、Web AppからNative Appに時代の流れが移行しつつある中まだ、一般的ではありませんが、今後のServer-Client型のサービスにおいては十分にメリットあります。


## クライアントでのマイグレーション

### データの移行方法
クライアントで行うマイグレーションの手順は非常にシンプルです。
**既存のデータを吸い上げてFirebaseに保存する。**これだけです。マイグレーションの時間は現行のいかなるサービスであっても約30秒弱で完了すると考えています。ユーザー一人当たりが保持しているデータは多くても1万件程度だと思います。Twitterを例に考えても数万件ツイートをしているユーザーは稀です。

`マイグレーションにかかる時間 = データの総数 / 1回で取得するデータ数 x データ取得にかかる時間 x 1回で取得するデータ数の保存時間`

`30s = 10,000レコード / 100レコード x 0.3s x (0.01s * 100レコード)`

※ネットワーク環境に依存します。

より具体的にイメージするために既存のサービスはREST APIを利用していると仮定して概念図を示します。

<img width="538" alt="スクリーンショット 2016-12-06 12.19.59.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/f373fc6c-db9e-7962-b5a0-512d3605877c.png">

1. Firebaseにデータが存在しないこと確認
2. 既存のREST APIでデータを取得
3. 取得したデータをFirebaseに保存

基本的な概念はこれだけです。

### マイグレーション中の運用
マイグレーション機能を実装したクライアントをリリースする前に、サーバー側にも手を入れておく必要があります。
マイグレーション機能を実装していないクライアントがしばらくアクセスを続けるためです。
新しく記録されるレコードはFirebaseにも書き込む必要があります。

<img width="729" alt="スクリーンショット 2016-12-06 12.40.04.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/2e9d0bb2-d845-76da-984a-4e6e059b8cd2.png">

### データ移行の注意
ここまででなんとなく、クライアントでのマイグレーションに関し、なんとなくうまくいきそうなイメージが湧いてきたのではないでしょうか？
しかし、Firebase Realtime DB特有で起こる幾つかの問題を考慮しなければなりません。

#### データのソート問題
この問題はFirebase Realtime DBがNoSQLであることと、[Rule](https://firebase.google.com/docs/database/security/)のセキュリティに設定によって起こります。
`User`と`Photo`の2つのモデルを使った構造を例に考えます。

まずこの問題を解決するために、Firebaseを使ってデータを保存する場合2通りの保存方法を理解する必要があります。
<img width="896" alt="スクリーンショット 2016-12-06 17.10.25.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/10f7ed31-21ba-563e-a3c4-058ec0fa053b.png">

| タイプ |　説明　|
| --- | --- |
| ネストして保存するタイプの利点・欠点 | UserはPhotoの実態を保持しているSort機能を利用できます。<br>Firebaseではネストしているオブジェクトへのアクセスは性能劣化を起こします。（実際このくらいのネストなら大丈夫）、Photoを他のユーザーから参照したい時、Userを経由するためセキュリティがややこしくなる
| 並列に保存するタイプの利点・欠点| Userをセキュリティを保ったままPhotoを共有できます。<br>UserはPhotoのkeyしか保持していないのでSortできません。 |

写真を共有するようなAppであれば、セキュリティを容易に担保できる並列に保存するタイプの方が良さそうです。
しかし、ソートの問題は解決しなければなりません。

##### なぜ並列ではソートできないのか
並列に保存されている`Photo`はそのオーナーがどの`User`か一見わかりません。`Photo`のオーナーを確認するためには、`Photo`内部のデータにアクセスする必要があります。`Photo`の内部を見ることができるのは`User`だけのはずです。つまり`Photo`をソートしようにもアクセス権を持たないのでできないのです。

ここでのルールは次のようになります。

``` Rule

{
  "rules": {
    "$version": {
      "user": {
    			"$user_id": {
    			".read": "auth != null && auth.uid == $user_id",
    			".write": "auth != null && auth.uid == $user_id",
    			}
      }, 
      "photo": {
    			"$photo_id": {
    				".read": "auth != null && (data.child('owner').val() == auth.uid || data.child('shared').contains(auth.uid))",
    				".write": "auth != null && data.child('owner').val() == auth.uid"
    			}
      }
    }
  }
}

```

#### ソート問題の解決

解決方法は2つ
1. Keyをソートに利用する
2. ソートに必要なデータをPhotoのKeyのValueとして含める

##### Keyをソートに利用する
Firebase Realtime DBはQueryで指定しない場合`Key`によってソートされています。Keyをうまく設定することでこの問題を解決します。
`Key`は辞書順でソートされます。また、Firebase Realtime DBのインデックスとして機能しているので、設定にはきをつける必要があります。
今回は次の規則に従い`Key`を設定しました。
`user_id` + `timestamp`
こうすることで、ユーザーは日付順にソートしたデータを受け取ることができます。残念ながらこれ以上のソート機能は望めません。これ以上のデータの操作を行いたい場合は、ネストして保存するタイプを選択する必要があります。
データをネストしたとしても工夫次第でユーザーの情報を隠した実装は可能です。
ただし、どのデータの下に、なんのデータが入っているのかを表現することが難しく大規模な開発になるに連れて実装が難しくなると考えています。

##### ソートに必要なデータをPhotoのKeyのValueとして含める
```
// 一般的なFirebaseのリレーション
users
  $user_id
    photos
      $photo_id: true
```
```
// ソートを考慮したFirebaseのリレーション
users
  $user_id
    photos
      $photo_id
        created_at: timestamp // created_atをソートに利用（ルールでもここをインデックスにしましょう）
```

### マイグレーションの後片付け
いつ起動されるかわからないクライアントだけでは、マイグレーションは完結しませんので注意してください。
残ったデータは必ず、サーバーでマイグレーションを行う必要があります。
数ヶ月のレプリケートの状態を経て、サーバーに残ったデータをFirebaseマイグレーションして完了です。