Cloud FireStoreを検討しよう
今もLGTMとストックがボチボチくるので...(全くアップデートできてなくてすみません)
本記事はFirebase Realtime Databaseの話をしています。
現在はFirestoreがリリースされており、以下で考察していた問題は別の方向で解消しています。
(QueryやCollection型)
Realtime Databaseも使いどころは残ってますが、通常のアプリの多くはFirestoreを使う方が楽に開発できる所感です。詳しい記事を出されてるのでそちらを参照くださいmm
実践的なFirebaseのDatabaseを考えよう。
最近、趣味の開発ではFirebaseを使用しています。
理由はmBaasとしてはもちろんですが、Hostingとしても使えて、クライアントサイドのみでアプリを完成させることができるからです。
しかしながら、チュートリアルなアプリならともかく、実用ともなるとハードルがあがるのが印象です。
私ごとに限らずですが、RDB脳でNoSQLなFirebaseのDatabase設計に挑むと痛い目を見るからです。
詳細は以下を一読するとよいと思います。実践導入の際の考慮することが非常にわかりやすいです。
Firebase Realtime DBの実践投入するにあたって考えたこと
Firebaseデータベースの効率的なデータ構造と高速化のポイント
アプリケーション開発ではほとんどRDBを使用しているのですが、NoSQLと真剣に向き合った時以下の問題にぶつかりました。
備忘録を込めて以下を一つずつ紐解いていきたいと思います。
- リレーショナル(データ構造やPK,FKの存在など)
- 正規化と非正規化
- トランザクションによるオールオアナッシング
- 制約
Awesome Firebase Youtube Channel
FirebaseではYoutube Channelを開設しており、開発について詳しく解説してくれています。
その中でも、The Firebase Database For SQL Developersは特にFirebase DatabaseがRDBと同じような機能を有するにはどうしたら良いか?を解説してくれているので一見の価値があります。
今回は問題についてどの回を見れば良いかを載せていこうと思います。
例題
例としてタスク共有管理のアプリを考えます。要件を以下とします。
- アプリケーションというルートが存在する。
- アプリケーションとユーザーは1:Nである。
- タスクはアプリケーション単位で投稿される。(つまり誰が投稿したかは問題ではない)
- しかし、タスク完了は誰が行ったかが必要である。
イメージしやすいようテーブル構造の例を挙げてみます。
RDBのテーブル構造例
アプリケーションテーブル
id | name |
---|---|
1 | 共有タスク管理1 |
2 | 共有タスク管理2 |
ユーザーテーブル
id | authId | appId | name |
---|---|---|---|
1 | xftaucns7-d | 1 | ユーザー1 |
2 | scubeycd-d | 1 | ユーザー2 |
タスクテーブル
id | appId | name |
---|---|---|
task1 | 1 | ゴミ出し |
task2 | 1 | 漫画買う |
task3 | 1 | 犬の餌やり |
完了タスクテーブル
id | taskId | userId |
---|---|---|
1 | 1 | 1 |
2 | 2 | 1 |
見慣れた構造じゃないでしょうか。テーブル毎に正規化して、外部キーでリレーショナルします。必要に応じて結合して取得します。
NoSQL(Firebase)のドキュメント構造例
{
"application":{
"1": {
"appName": "共有タスク1",
},
"2": {
"appName": "共有タスク2",
}
},
"task": {
"task1": {
"taskName": "ゴミ出し",
"applicationKey": "1",
},
"task2":{
"taskName": "漫画を買う",
"applicationKey": "1",
},
"task3":{
"taskName": "犬の餌やり",
"applicationKey": "1",
},
},
"doneTask": {
"task1": {
"taskName": "ゴミ出し",
"userName": "たかし",
"applicationKey": "1",
"uidKey": "xftaucns7-d",
},
"task2": {
"taskName": "漫画を買う",
"userName": "たかし",
"applicationKey": "1",
"uidKey": "xftaucns7-d",
},
},
"user": {
"xftaucns7-d": {
"userName": "たかし",
"applicationKey": "1",
},
"scubeycd-d": {
"userName": "ひとみ",
"applicationKey": "1",
},
},
"applicationHasTask":{
"1":{
"task1":true,
"task2":true,
}
},
"applicationHasDoneTask":{
"1":{
"task1":true,
"task2":true,
}
},
"applicationHasUser":{
"1":{
"xftaucns7-d":true,
"scubeycd-d":true,
}
},
"userHasDoneTask": {
"xftaucns7-d": {
"task1": true,
"task2": true,
}
},
}
上記の様に設計できると考えています。
NoSQLはドキュメント指向です。簡単に言えばKey/ValueペアのJSONです。
NoSQLにはスキーマという概念がなくRDBに比べ厳密性はありませんが、柔軟にデータの拡張を行うことができます。
しかしながら、RDBの優れた点を捨てた時、
アプリケーションの設計をどうやるか?データの整合性をどう保つか?
がぶち当たった問題であり、それらを解決するためにFirebaseでは方法を解説してくれています。
※ これらは例であり、現在も取り組んでいる問題です。申し訳有りませんが、誤りがあった場合は訂正します。
リレーショナル(データ構造やPK,FKの存在など)
問題
Firebaseはドキュメント指向のNoSQLの一種です。
Key/Valueのペアで保存される場合、PKやFKはどの様に表現するのでしょうか?
また、どの様な構造が適しているのでしょうか?
解説回
解決方法
以下の件が解決方法になるのではないでしょうか?
PK・・・親のキー
FK・・・ルックアップテーブル
データ構造・・・平坦化
上記データを参考に考えます。
PKは親の"1"や"task1"などがPKとなります。これらはJSONで言うキーです。同階層でユニークとなります。
FKはルックアップテーブルになります。Firebaseでは平坦化を推奨しており、以下のようにルックアップテーブルを用いてテーブル同士を結合することができます。ルックアップテーブルは1:Nのリストの様な親子関係だと理解しています。
今の所、N:Nは出てきていませんが、その時はどうすれば良いかわかりません。
また、外部キーが出来るたびにルックアップテーブルが増えていくのかぁ。と考えると、
結構、うーーーーーーーーーーん。な設計になりそうです。。。
私は今の所良い解決方法が見つかっていません。
{
"application":{
"1": {
"name": "共有タスク1",
},
"2": {
"name": "共有タスク2",
}
},
"task": {
"task1": {
"taskName": "ゴミ出し",
"applicationKey": "1",
},
"task2":{
"name": "漫画を買う",
"applicationKey": "1",
},
"task3":{
"name": "犬の餌やり",
"applicationKey": "1",
},
},
"applicationHasTask":{
"1":{
"task1":true,
"task2":true,
}
},
}
平坦化というキーワードが出てきましたが、上のデータ構造は極力、データ構造を浅く保つことを意識しています。下記の様に定義するのが直感的ですが、ネストが一つ増えてしまいます。
更にユーザーによって分けたいなど要件が増えると、ネストがどんどん増えていきます。その様な設計を避けるのが、設計の複雑さの回避や、Firebaseのパフォーマンスにも繋がります。
{
"task":{
"1": {
"task1": {
"taskName": "ゴミ出し",
"applicationKey": "1",
},
"task2":{
"name": "漫画を買う",
"applicationKey": "1",
},
},
"2":{
"task4":{
}
}
}
}
正規化と非正規化
問題
正規化はRDBでテーブル設計を考える時の基本です。「データの重複は極力辞めようね。」というモノで、RDBは散らばったデータをキーを用いてリレーショナルで解決します。
NoSQLにはリレーショナルはありません。(擬似的に作ることはできますが)その場合は正規化はどのように考えるのでしょうか?
解説回
解決方法
Firebaseを最大限のパフォーマンスを発揮するには、非正規化こそ、正規化と言っています。
つまり、取得の際はデータを結合せず一度のパスで取得できるような設計にすることで、データの取得が早くなるということです。上記のケースではデータを重複させています。
{
"task": {
"task1": {
"taskName": "ゴミ出し",
"applicationKey": "1",
},
},
"doneTask": {
"task1": {
"taskName": "ゴミ出し",
"userName": "たかし",
"applicationKey": "1",
"uidKey": "xftaucns7-d",
},
},
}
ただし、その際に疑問に浮かぶのが、
1.いつ非正規化を開始するのか?
2.どのタイミングで行うのか?
動画の解説では、経験則上ビューを作った後、必要あれば非正規化する。と述べてします。
私なりの理解をすると、「早さと便利さはトレードオフで、必要あれば非正規化しようーぜ。」とのことかと思います。
肌感ですが、RDBとNoSQLの違いはデータ設計のアプローチなのか、コンポーネント設計のアプローチなのか」の違いがありそうです。
正規化と非正規化をごちゃまぜにすると精神衛生上気持ちわるいので、どちらかに舵を切る様な気がします。
非正規化はかなり回りくどいやり方に感じますが、モバイルを考えた際は有効です。
例えばスクロールの度にデータを結合して取得するのは処理負荷がかかります。それは、データが多くなれば多くなるほど検索効率が下がります。
郷に入れば、郷に従う形で考えていくのが良いのではないか。と言うのが所感です。
トランザクションによるオールオアナッシング
問題
所謂、ACID問題です。
解説回でも述べていますが、データベースの更新は全てクライアントサイドで行います。
この間にアプリが終了されたり、不具合により処理が終了した場合はどうなるでしょうか?
恐らく、整合性の欠損が起こります。
また、各所に保存されているデータの同期を取る方法はあるのでしょうか?
解説回
解決方法
ルックアップで結合してマルチパス更新を行うとのことです。
以下はルックアップから動的にパスを作成し、マルチパス更新を行うためのメソッドです。
マルチパス更新により原子性が保証されます。
気になるのが、マルチパスの更新が大きくなる可能性があることです。
その場合はサーバーサイドにFirebaseを導入してください。とありますが、可能な限りクライアントサイドで完結させたいです。
非正規化を行わず、キーで持つのが良いのか? トレードオフで考える必要がでるのでしょうか。
制約
問題
制約とはデータを限定する方法です。意図しないデータに対して検査(Validation)し、データの整合性を保ちます。Firebaseにはスキーマがないため、これが可能か問題になります。
解説回
解決方法
これは、明確な方法がありそうですね。
FirebaseのRuleを使用してください。とのことです。
セキュリティとルールを参照のように、.validate
で検証をすることができます。
備考
NoSQLについて勉強してみて、当たり前に使用していたRDBについての理解が深まったように感じました。
テーブル定義も、正規化も、制約も全てはデータの整合性を保つためにあるものなのです。
NoSQLは制約がない分、自由に設計することが可能です。ただし、その設計方法は開発者に委ねられます。
ただし、RDBにおいても綺麗なテーブル設計を出来る開発者は少ないですし、NoSQLだけが面倒なわけではなく、同様に理解と経験が必要ということでしょう。
終わりに
reduxでテストしながらベストプラクティスを探っている状態です。
ORMみたいなのないですかね?もしくはミドルウェアとか? どなたか知りませんか? (脱RDB脳と言っておいてw)