この記事はSFC Advent Calendar 2020 13日目の記事です
日々アイデアのプロトタイピングに追われているSFC生(もちろんそうでない方も)のために,すぐに使えるDBであるKVS,得にFirebase Firestoreを使うときのメリットとデメリットを紹介したいと思います.
SQLよりは気楽に使えそうだけど,本当に使って大丈夫?後で困らない?と思っている方が主な対象で,こういう開発には使わない方がいいよという話を中心に書いていきます.一方で具体的な使い方については述べないので公式ドキュメントをご覧ください.
筆者自身はRealtimeDatabaseの頃から4-5年使っていますが,ちゃんとしたちゃんとした理論を学んだわけではないので訂正等あれば是非お寄せください.
目次
- なぜFirestoreを使うのか
- Firestoreって大丈夫なの?
-
Firebaseのデメリット
2. [ドキュメントへの書き込みは1秒に1回まで] (#ドキュメントへの書き込みは1秒に1回まで)
3. SUMやAVERAGEなどを計算した結果・ドキュメントの数を求められない
4. テーブルを結合(join)できない - 補足:CAP定理
- まとめ
なぜFirestoreを使うのか
FirestoreにはFirebaseというmBaasの一部であることに由来する「Firebaseという強力なプラットフォーム」という側面と「KVS」という側面があります.それぞれのメリットを見ていきましょう.
Firebaseという強力なプラットフォーム
- 黒い画面をCUIでぽちぽちしなくてもマウスで立ち上げられる
- Web上で簡単にDBの内容を編集
- Web上で簡単に設定できるアクセス制御
- 基本的に無料
- Firebaseの他の製品群との連携(Authentication / Cloud Functions)
- Firebaseが提供するモバイルアプリ・Webアプリ向けライブラリ群の存在
上から3つはコンソール上での設定操作が不要という話です.コマンドを全く知らなくてもすぐに使い始められるのは,(CUIのほうが速いという方もいるとは思いますが)間違いなく便利です.とりあえず保存したいときにサーバーを用意して色々インストールしなくても1分で使い始められます.
料金については,基本的に無料枠内で使用できます.少なくともプロトタイピングで課金しないといけなくなったことはありません.
Firebaseの他の製品群との連携については,Auth周りの知識が無くても指示に従って設定すれば簡単にユーザーのログインが必要なアプリを開発できますし,Cloud Functionsと連携(何らかのトリガーでFirestoreを動かす or Firestoreの操作をトリガーにしてFunctionを動かす)させれば「新規にユーザーが作成されたら自動的にユーザーのプロファイルをDBに作成する」みたいなこともできます.
モバイルアプリ・Web向けライブラリは本当に素晴らしいです.これがあるからFirebaseをつかっていると言っても過言ではありません.例えばiOSならばSwiftからあたかもローカルデータベースを触っているかのようにデータの操作ができるというのはもちろん、ほとんど設定せずにオフラインで使えたり、リアルタイムで同期できたりします.APIを叩いてサーバーと普通に通信すると,簡単にはサーバー側の変更を自動で反映するみたいなことはできないのでこの機能は助かります.また,Swift向けにCodableに対応させることができるほか,gPRCのサポートやRESTでのアクセスも可能です.
KVS
FirestoreはnoSQLであり,KVSです.KVSという言葉を知らなくてもjson形式とかディクショナリなら使ったことがあるでしょう.Firestoreはそれらとほぼ同じなのですが,少し使い方に癖はあります.
ちなみに,Firebaseの前バージョンであったRealtime Databaseについてはほとんど完全にディクショナリだったのですが,それだと大量のデータを持っているドキュメントの親を取得するとその子孫が全て取得されてしまい効率が悪いという問題がありました.Firestoreでは簡単に全ての子孫を取得できなくなった代わりにそのあたりの問題が解消されています.具体的な内容は使い方記事ではないので触れません.
- データ定義 == データ作成(事前に定義する必要がない)
- 柔軟なデータ構造
- 高いスケーラビリティ
- 高速
これらのメリットは主にRDBとの比較です.
まず1つめ,データ定義が不要というのと,柔軟なデータ構造というのは似たような意味ですが,これはメリットであり同時にデメリットです.デメリットについては後ほど述べますが,少し保存したいというときにはきちんと設計せずに気軽に始められるので便利です.SQLのようにデータ定義構文を憶えなくても,最低限データの保存方法と取り出し方を覚えれば使えてしまいます.筆者はそこそこ大規模な開発をする場合はKVSでも設計ダイアグラムを書きますが,そうでなければFirebaseのWebエディタ上でサンプルデータを作ってしまうこともよくあります.
3つめのスケーラビリティはRDBと違ってKey-Value構造だとデータを分散できるということに由来します.まあFirebaseを使えばその辺は勝手にやってくれるので分散してるのかどうかさえわかりませんが.
高速な理由はスケールアウトできるからだと思います.大量のアクセスが見込まれるイベント用システムを開発する場合など,アクセス集中を想定したテストは難しいので特に何もせずいい感じに処理してくれるのは心強いですね.(このあたり,筆者はあまり考えることがないので適当でごめんなさい)
Firestoreって大丈夫なの?
数年前にFirestoreに出会ったとき,「Firestore最強じゃん!!」って思いました.しかし同時に「じゃあなんで未だRDBが主流なの? 何か裏があるのでは?」とも思いました.実際Firestoreが向いていない場合もあります.Web上にもメリット・デメリットの記事が載っていますがKVS自体のデメリットをFirestoreの機能で補っているなどアップデートされて改善されている部分も多いので改めてまとめてみたいと思います.
Firebaseの限界
ここからはこちらの資料「NoSQL Data Modeling Techniques」あるいは日本語版の「NoSQLデータモデリング技法」を参考にしました.もともとの資料は大変よくまとまっているので時間がある方は読むことをお勧めします.
ドキュメントへの書き込みは1秒に1回まで
FirestoreのKey-Valueのツリー構造はドキュメントというものとコレクションというものが交互に来るようになっています.例えばusersというコレクションにuser_id_1,user_id_2...みたいな名前のドキュメントが並んでいるイメージです.さらにドキュメントはコレクションとフィールドを持つことができます.この制約はドキュメントが持っているフィールドを変更する動作を,同じドキュメントであればフィールドが異なっていても1秒に1回以上できないという意味です.不特定多数の人が同じドキュメントにアクセスする可能性のある設計にしなければそんなに問題にはならないでしょう.逆に1人または数人のユーザーがどんどん情報を書き換えるゲームのオンライン対戦みたいなデータは当然向いていません.
SUMやAVERAGEなどを計算した結果・ドキュメントの数を求められない
SQLなら条件にマッチする行の平均値や合計値,マッチした数を簡単に調べられますが,Firestoreではそれができません.これらをやりたければ二種類の方法があります.
- 条件にマッチする全てのデータを取得してクライアント側で合計・平均・ドキュメント数を数える
- 「sum」「number_of_contents」などのフィールドを用意しておき,書き込むときにそれらをインクリメントする
一つ目の方法は簡単ですがデータ量が膨大になると使えません.後者は面倒ですが安全でしょう.ちなみにインクリメントにはトランザクション処理を書く必要がありますが,Firestoreには**FieldValue.increment()**という関数が用意されており,通常の書き込みと同じように書けます.
テーブルを結合(join)できない
followersというフィールドにuser_idのリストがあるようなデータがあれば,user_idの部分にそのユーザーのデータを入れて返したいことがあるでしょう.SQLではこのような操作は普通にテーブルの結合として行うことができますね.しかし,KVSでは結合できないのでfollowersのリストをまず取得し,クライアント側でリスト内にあるuser_idのデータを一つずつ取得する必要があります.(アプリケーションサイドjoin)(一応「リストにあるデータを一括して取得する」というクエリも用意されていた気がします)
クライアントサイドでjoinするというのはRDBに慣れていると気持ち悪いかも知れませんがKVSの思想としてそれで問題ないようです.
しかしjoinするデータが多くなると取得する処理が多くなり大変ですね.解決策は,RDBの書き方を捨てることです.ケースバイケースですが,同じ情報をいろんな場所に書いてしまう,つまり正規化しないのはKVSではアリです.当然データがアップデートされたときに全ての部分で変更しないといけないのでその処理を上手くするひつようがありますが,常に同期されていないと深刻なエラーが起きるようなものでなければそれでよいでしょう.
Firestoreにはもう1つ使い方が限定的ですが設計を見直すことで解決できる場合があります.例えばTwitterのようなデータベースをつくるならどうするでしょうか.まずルートにusersとtweetsというコレクションを作り,ユーザーのツイート一覧を表示するときにはtweetsの中から投稿者idでfilterするかもしれません.この設計でもよいのですが,userドキュメントそれぞれにtweetsというコレクションを持たせることも考えられます.userドキュメントが肥大化しそうですが,ドキュメントが持っているコレクションは内部的に独立しているのでuserドキュメントを取得したときに全てアクセスされることはありません.ユーザーを横断的にtweets全体を取得したい場合にはcollection groupという機能を使い,tweetsという名前の全てのコレクション全体を取得することができます.
まとめるとテーブルを結合できないことの解決策は次のようになります.
- アプリケーションサイドjoin
- 非正規化
- 設計の見直し
補足:CAP定理
KVSとRDBどちらがよいのか,と考えるときにはCAP定理が役に立ちます.WikipediaあるいはBrewer's CAP TheoremによるとCAP定理は以下のように説明されています.
ノード間のデータ複製において、同時に次の3つの保証を提供することはできない.
一貫性 (Consistency)
ノード間のデータ複製において、同時に次の3つの保証を提供することはできない。
すべてのデータ読み込みにおいて、最新の書き込みデータもしくはエラーのどちらかを受け取る。
可用性 (Availability)
ノード障害により生存ノードの機能性は損なわれない。つまり、ダウンしていないノードが常に応答を返す。単一障害点が存在しないことが必要。
分断耐性 (Partition-tolerance)
システムは任意の通信障害などによるメッセージ損失に対し、継続して動作を行う。通信可能なサーバーが複数のグループに分断されるケース(ネットワーク分断)を指し、1つのハブに全てのサーバーがつながっている場合は、これは発生しない。ただし、そのようなネットワーク設計は単一障害点をもつことになり、可用性が成立しない。RDBではそもそもデータベースを分割しないので、このような障害とは無縁である。
RDBは単一サーバーで動作するため分断耐性を犠牲にして一貫性と可用性をとっているといえます.一方,KVSは分散しやすくすることで分断耐性を持っているかわりに一貫性または可用性を失っています.一貫性か可用性かどちらを犠牲にしているかという話ではKVSのシステムによるわけですがFirebaseの場合は一貫性を犠牲にしているといえます.
CAP定理はデータベースシステムの基本的な部分については成立しますが,完全にこれに縛られているというわけではなく,一時的にどれかが犠牲になるにせよ最終的に全てを満たせるようになっている場合もあります.Firestoreの場合は,トランザクション機能があることにより一貫性を保つような操作も可能です.
※細かいところに突っ込みすぎるとややこしくなるのでふんわりとした説明になっています
まとめ
KVSをはじめて最初に立ち塞がる壁はjoinできないということかと思います.むしろ完全なプログラミング初心者がデータ永続化したいなと思って使うときにはほとんど不自然さを感じないのかも知れません.
joinできない問題について記事内では3つの考え方を紹介しました.アプリケーションサイトでjoinすること,非正規化すること,設計を変えてみることです.二つ目と三つ目は似たようなことですが.
(RDBに比べて)最近はやり出したサーバーサイドKVS(?)ですが,順番的にはKVSのほうが古典的なデータベースです.どちらがよいかというとCAP定理からわかるとおり,それぞれ一長一短あるもので,上手く使い分ける必要があります.
KVSのスケーラビリティは大規模システムで効果を発揮しますが,mBaasの特性から小規模なプロジェクトでこそ便利なサービスになっています.KVSとFirestoreのクセを理解して使えば小規模プロジェクトから大規模なものにまで有用だと思います.