Firebaseのマイグレーション
前回Firebase.yebisuで質問を頂いた。Firebaseのマイグレーションについて言及します。
TL;DR
マイグレーションはスキーマ定義が汚れてもいいから新しいプロパティをはやすことで頑張る!
ビジネスとDBマイグレーション
マイグレーションについて話を進める前にマイグレーションはどのようなシーンに置いて必要なのか私の考えを述べようと思います。
一言でマイグレーションと言ってもその規模は様々で今から話をする内容がどの規模の物かを想像するのは、その人の置かれた状況や立場によって考え方が変わると思います。
- 既存DBからのFirebaseへの移管
- FirebaseからFirebaseへの移管
- スキーマの変更
そんな中まず最初に述べたいのは、マイグレーションは実施しないに越したことはないと言うことです。
なぜならリスクが必ずついてくること、リスクに対してのリターンが見合うかはやってみないとわからないことの方が多いことが理由です。
マイグレーションを行う際には、技術的な検証よりもビジネス的な検証をまず優先することをオススメします。リスクとったけどビジネスにならないのでは、とんだ骨折り損です。
既存DBでサービスがまわっているのであれば移管しなくともFirebaseにはカスタム認証システムと呼ばれるものがあるのでこれを使って両方のDBを共存させるのが有効だと思います。
時代によって新しいDBはどんどん出てくるのでDBを統一化してしまうより、インターフェースを揃えてカプセル化してしまう方がよっぽどエンジニアフレンドリーに開発を進めることが出来ます。
MySQLであろうと、DynamoDBであろうと、Firebaseであろうと、サービスを利用するユーザーには全く関係ないのでDBのマイグレーションについては特に慎重に判断して下さい。
と言うことでここではビジネス影響が少ない範囲について説明していきます。
マイグレーションについて
FirebaseはスキーマレスDBですが、スキーマを設計して開発を進める方が圧倒的に効率的ですので複数のモデルを先に定義してしまいましょう。
今回は下記の3つで話を進めて行こうと思います。
- User
- Group
- Item
また加えてiOS向けのModel FrameworkであるPringの内部についても言及します。iOS向けではあるものの構文も非常に似ているのでTypeScriptやKotlinで同じものを構築していくことが可能だと思います。
スキーマ定義
User
class User: Object {
var status: Int = 0
var name: String = "unknown"
var friends: ReferenceCollection<User> = []
var groups: ReferenceCollection<Group> = []
var items: NestedCollection<Item> = []
}
Group
class Group: Object {
var name: String = "unknown"
var onwner: Reference<User> = Reference()
var users: ReferenceCollection<User> = []
}
Item
class Item: Object {
var name: String?
}
Pringの機能について
Pringでは開発を効率的行うために以下の機能を持っています。
マイグレーションについてのみ理解を深めたい方は飛ばしてもらって構いません。
関係性 | 機能 | 説明 |
---|---|---|
1:1 | Reference | 1:1の関係を表します。Salada では関係性をIDで管理していましたが、関連したインスタンスを保持できるようになりました。 |
1:N | NestedCollection | 1:Nの関係を表します。名前の通りネストされた状態で保存されます。上記の定義ではItemは上位ノードでは保存されず、 Userの下に配置されることとなります。Itemにアクセスするためのパスを示すならば次のようになります。 /user/:user_id/items/:item_id |
1:N | ReferenceCollection | 1:Nの関係性を示します。NestedCollectionと違うのは上位ノードでモデルが保存されることです。 Userがもつfriendsにアクセスするためのパスを示すならば次のようになります。 /user/:user_id/friends/:user_id/ ただしここには値が存在しないため、 /user/:user_id/ アクセスする必要があります。 |
Userのマイグレーションについて考える。
Firebaseのマイグレーションで考えられるのは以下の3つのパターンです。
パターン | 説明 |
---|---|
ADD | 新しいプロパティを追加する |
MOD | 既存のプロパティの型を変更する |
DEL | 既存のプロパティを削除する |
それぞれについて考えて行きましょう。
ADD
どんな作業が必要か?
Firebaseはスキーマレスなので大きな作業は必要になりませんね。
サービスへの影響は?
ADDは基本的に新しい機能を追加する場合に起るマイグレーションパターンです。新機能を使い始めたときにプロパティーが追加されさえすればこちらもなんの問題もないと思います。
MOD
どんな作業が必要か?
MODのイメージを具体化させるために例をあげてみます。
Userのstatusは今Int
で定義されていましたがわかりにくかったのでString
に変更することにしました。
作業ストラテジーは2つ考えられます。
- MODもADDにしてしまう。
- MODする。
MODもADDにしてしまう
MODもADDにしてしまうとはつまり、新しいプロパティとしてstatusを定義してしまう方法です。そうなると次のような感じになります。
class User: Object {
var status: Int = 0
var statusStr: String = "none"
var name: String = "unknown"
var friends: ReferenceCollection<User> = []
var groups: ReferenceCollection<Group> = []
var items: NestedCollection<Item> = []
}
運用でカバーってやつですね。遺産としてstatusは残りますが、まぁ固いこと言うなよ。正直これでいいと思ってます。
MODする
問題は本気でMODする場合ですね。
ここでも作業ストラテジーは2に考えられます。
- 既存データを全て置き換える
- 必要なデータを置き換える
既存データを全て置き換えるのは古典的な方法ですね。他のDBでマイグレーションと言うと基本的にこれを想像するはずです。しかし現代では不可能に近い方法です。FacebookやTwitterを想像すればその理由はわかると思います。
データの増加量にマイグレーション量が追いつかないと思います。
必要なデータを置き換えるのが現実的です。必要なデータとは、つまりアクセスされたデータのことを指します。データにアクセスされたらそのときにデータをマイグレーションしてしまいましょう。このとき注意しなければならないのがバージョン管理することです。古いバージョンのアプリからもアクセスがあることを考えると古いデータも残して置く必要があります。
そしてデータはDouble Writeが必要になります。
Modelをバージョン管理してマイグレーションを行うマイグレーションチェーンについては後ほど詳しく説明します。
サービスへの影響は?
選ぶストラテジーによって影響は異なりますが、本気でMODする場合はマイグレーションの時間分少しレイテンシが発生します。いずれにしてもサービスを止めることなく作業が行えるのでサービスには大きな問題はないと思います。
DEL
DELに関してはやらないと割り切ってしまう方がいいでしょう。基本的に論理削除のみを行い物理削除はやらない方がいいです。FirebaseにはDBの妥当性を担保する方法がありません。
どうしてもやった方がいいと意見をお持ちの方はぜひその理由を教えて欲しいです。
マイグレーションチェーン
マイグレーションチェーンの概要に説明します。
マイグレーションチェーンは必要なデータのみマイグレーションを行うというストラテジーを基にデザインされています。
マイグレーション後の理想な姿は、Appがそれぞれのバージョンでバージョンを参照している状態です。
しかし、必要なデータのみマイグレーションを行うと言うことは、まだマイグレーションが終わってないデータが存在することもありえます。
理想
現実
このとき、マイグレーションを再帰的に行います。これがマイグレーションチェーンです。
マイグレーションチェーンを利用するためにはいくつか制約があります。
- データは必ずいずれかのバージョンに存在する
- データはどのバージョンでも同じIDを利用している
- 下位バージョンが変更されると上位バージョンに伝達される
データは必ずいずれかのバージョンに存在する
データがどのバージョンにもない場合は、データが削除されたことを意味します。上の図のApp3.0からリクエストがコールされた場合毎回マイグレーションチェーンが走ることになるので必ずデータを保持するようにしてください。
データはどのバージョンでも同じIDを利用している
データはIDによって管理されるため、IDは必ず同じものを使う必要があります。そうするとデータへアクセスするときはversionの番号だけ変更してあげればよくなります。
/version/1/model/:model_id/
/version/2/model/:model_id/
Pringではバージョンを定義できるようになっています。
modelVersionをオーバーライドすることでバージョンを管理する。
詳細はこちらから
https://github.com/1amageek/Pring/blob/master/Pring/Object.swift#L15
class User: Object {
class var modelVersion: Int {
return 1
}
}
下位バージョンが変更されると上位バージョンに伝達される
上位バージョンが存在するデータではすでに新しいバージョンでのデータを利用した運用が始まっていると言うことです。この時古いバージョンのデータが更新されれば必ず上位バージョンまでその変更を伝えてあげなければなりません。
また上位バージョンが変更された場合も下位バージョンに伝達する必要がありますが、これを実装するくらいならば、アップデートを促す仕組みを取り入れた方がいいと思います。
まとめ
マイグレーションチェーンの運用しますか?やらないですよね?
新しいプロパティを運用しましょう。
Pringにはマイグレーションチェーンの機能を実装しようと考えてますが。
ではでは素晴らしいFirebase Lifeをお楽しみください。