こんにちは。virapture株式会社のもぐめっとです。
最近ユニクロで友達とオソロのメタモンTシャツ買いました。カワイイです。
本日はfirestore使ってて辛いよーという声をよく聞いたので、そのままfirestore使っていると危険な理由と対策など4つのアンチパターンとして紹介しようと思います。
1. Join Lover: データをjoinする
目的
RDBではよくあるテーブル同士を結合してデータを取り出すJoin。
firestoreでjoinを用いたいケースというのは特定のドキュメントのデータだけでは表示する要素が足りないので別のドキュメントから取得してなんとかするみたいな感じになると思います。
しかし、firestoreのプロもおっしゃってますが、firestoreへのjoin追加は望みが薄いと思われます。
RDBで重くなってる要因も外部結合や副問い合わせとかガンガン使って重たいSQLが発行されて重くなるというサイトよくありますよね。。。
アンチパターン
firestoreでjoinをやる場合は違うコレクションのドキュメントを2回取得してクライアント側でごちゃごちゃやるといった形になるかと思います。
これによるとリードコストが2回発生するし、マージするといったロジックも入り込んでくるので、複雑なコードになっていき保守性も下がっていきます。
見つけ方
「マイページを表示する際に、userドキュメントで読み取った表示以外にも、ユーザが作った記事数の総数を表示しないといけないから記事Collectionから一括リードして数を数えないといけない。」
こんな会話が聞こえたら要注意。
ただ総数を数えるだけなのにリードコストも記事数分跳ね上がっていきます。
Joinを使っても良い場合
データ分析といった観点では複雑な要素を絡めあわせてデータを結合できるjoinが使えるRDBのほうにやはり軍配が上がります。
firestoreでデータ分析するといった場合は裏側でBigQueryに出力してBigQueryでガシガシ分析をするのがベストプラクティスになります。
最近はextensionで簡単にBigQueryにexportできるようになっているので導入も簡単です。
解決策
そもそもfirestoreはNoSQLなのでjoinするような仕組みにすべきではないのです。
そのため、RDBの進む方向とは反対に非正規化したり、必要要素をCloud Functionsで別途追加するなどが正解になっていきます。
今回の記事数の例でいうと、後者の対応になります。
userドキュメントにarticleCountというフィールドに対して、記事を作るたびにCloud Functionsでインクリメントしていくと、コレクションをリードせずとも記事数を取得することができます。
2. All Listener: 全データをListenする
目的
firestoreの最大の特徴として、リアルタイムにアップデートを入手できるonSnapshot()メソッドというものがあります。
これはドキュメントやコレクションに対して変化があれば随時その変化を送ってくれるというものになります。
チャットなどでよく使われる機能になります。
アンチパターン
チャットではコレクションの書き込みがすごい数になります。
そのため何も考えずにそのままonSnapshotすると全件リードになり、リードするのにすごい時間もかかり、読み取った後のクライアントの処理もすごいことになり、読み込み課金もすごい額になってしまいます。
しかも変更があるたびに変更データが通知されて、クライアントでまた処理するの大変重いです。
MySQLの実行計画でいうとtypeがALLと出て全件スキャンになっているようなものです。
見つけ方
「webにあったチャットのサンプル使ってとりあえずチャット実装できました!!!!!!1111111楽勝っす!!!!!」
こんな声が聞こえたら要注意です。
webにあるサンプルはlimitの考慮をせずサンプルが記載されてることがあります。
全データ取得しても良い場合
ListenするCollection自体が大した数ではなければ問題なくListenしてもよいでしょう。
どれくらいの数が想定されるかはアプリケーションの仕様と要相談になります。
例えば記事についているコメントとかであればそこまで大した数になることはないはずです。(炎上とかしてなければですが)
解決策
Limitをちゃんとつけてページネーションするようにしましょう。
チャットであれば下記実装をおすすめします。
- チャット画面を読み込んだ際に直近ある程度の件数(20件ほど)読み込む
- 読み込んだチャットをメモリ上に保管
- 最新の日付以降で直近のチャットをLimit3件くらいでListenする(同時発言3件くらいまで対応できる)
- 追加があったチャットのみをメモリ上の変数に随時追加
- チャットを再描画する
追加読み込みに関しては最後に読み込んだ時間手前以降のチャットを追加読込してメモリ上に追加していきます。
(ちょっと何いってんだという人のために、実装例を後日記事にします)
3. All For One Search: 検索を頑張る
目的
データがある程度増えてくるとそのデータにたどり着くための検索機能が欲しくなります。
賃貸サイトの検索とかは特に条件が多岐に及ぶので色々検索したくなりますよね。
アンチパターン
join使えるRDBはjoin頑張れば検索機能作れます。(パフォーマンスは度外視して)
ですが、firestoreはちょっと(というかかなり)検索というか絞り込み条件が弱いっす・・・
結論から言うと、firestoreに検索は向いてません。
向いてない理由はパッと出るものだけでも下記の3点(以上)あります
1. 全文検索はできません。
Like句はデフォでは用意されてません。
そのため後方一致や前方一致といった文字検索はそもそもできません。
2. orderBy()句に制限がある
・ orderBy() 句は、指定したフィールドの有無によるフィルタも行います。指定したフィールドがないドキュメントは結果セットには含まれません。
・ 範囲比較(<、<=、>、>=)のフィルタを含める場合、最初の並べ替えは同じフィールドで行う必要があります。
3.不等号の検索もフィールドが1つまでと限定されています。
範囲(<、<=、>、>=)または不等値(!=)の比較は 1 つのフィールドに対してのみ実行できます。複合クエリに含めることのできる array-contains 句または array-contains-any 句は 1 つだけです。
その他クエリ諸々の制限事項は下記をご参照ください
見つけ方
「男女のチェックボックスや、年齢、趣味、紹介文の抜粋などから検索したい!」
検索と聞いたらもう反応して問題ないでしょう。
かなり実装が大変になることが予期されます。
検索をしても良い場合
検索条件がシンプルなら問題ないです。
例えば
- 「年齢だけで検索して年齢順に並べる」
- 「男のみ検索する」
といった検索条件のフィールドが一つだけのシンプルな検索方法になります。
解決策
これは公式でも記載されてますが、検索は全部algoliaなどの検索プロバイダにぶん投げです。
最近はExtensionsも用意されてalgoliaの導入も簡単にできるようになりました。
また、全文検索したいけど、外部サービス使うほどじゃないなぁ。。。という方でしたら、こんな試みをしている人もいるのでこの方法を使えばfirestoreで完結することも可能そうです。
さらに、LIKE検索であればfirestoreの機能だけで実はできたりします。(少しハッキーな方法だが簡単にできる)
必要に応じた実装方法を選ぶといいと思います!
4. Intensive API: データの取り出しにAPIを挟む
目的
firestoreの実装上、クライアント側にデータ取得のロジックが入ってきます。
APIにしたらクライアントはAPI叩くだけでデータ取り出せるよねーといった用途でAPIを立てるケースになります。
アンチパターン
Cloud Functionsを使うことでAPIのようにコールアンドレスポンスで処理結果を返すことができるようになります。
しかし、これをすると下記3点のデメリットが考えられます。
1. セキュリティが脆弱になりうる
CloudFunctionsでは、セキュリティルールを介さずにfirestoreにアクセスできてしまうので、セキュリティに脆弱性がでてしまう場合があります。
2. CloudFunctionsのコールドスタートが遅い
CloudFunctionsはイベントベースでインスタンスが起動して処理をするものになるのですが、しばらくアクセスがないとインスタンスは停止してしまいます。
そのためインスタンスが起動してレスポンスを返すまでに時間がかかる可能性があるため、UXが非常に悪くなります。
3. firestoreの利点を活かせなくなる
firestoreの最大のメリットの一つである、クライアントから直接データにアクセスすることでサーバレスアーキテクチャを実現するといったメリットを活かせなくなります。
API化するとクライアントでのロジックは少なくなり、確かに集約するとは思いますが、実装コストと運用コストの兼ね合いを考えると、メリットが薄れてきます。
■実装コストの問題
CloudFunctionsは数が増えるとかなりデプロイに時間がかかってくるので、実装スピードが下がってきます。
無闇に増やさないようにしたほうがいいです。
今、運用しているCameconでは50近くのCloudFunctionsが動いてますが、すべてのデプロイが終わるのに30分くらいかかってしまっています
[番宣] 気軽にフォトコンテストに投稿できるので使ってみてね!
■運用コストの問題
CloudFunctionsが呼ばれるとその分課金もされていきます。
「じゃぁCloudFunctionsじゃなくてEC2とか別サーバ立てればいいじゃん」
という話もあると思いますが、今度はそのサーバの面倒をみないといけなくなります。
もしAPIサーバに高負荷がかかればオートスケールなどさせないといけなくなりますし、低コスト運用ができなくなります。
firestoreを使う意味がますます薄れていくことでしょう。
見つけ方
「フロントに何度もロジックを実装するのは実装コストかかるのでAPIに集約化しましょう!」
こんな話が出てきたらfirestoreの使い方を間違えている可能性が高いです。
APIを使っても良い場合
仕様として、処理した結果、エラーや何かしらのレスポンスが必要になってくるようでしたらCallable CloudFunctionsを考慮してもいいと思います。
また、あまりコールされず、処理が遅くても問題ないといったケースでも考慮してもいいと思います。
今、運用しているワンナイト人狼オンラインでは、下記2点の理由から課金処理のケースで使ったりしています。
- 裏側で購入の検証や、処理した結果などを返さないといけない
- 購入頻度は少ないため少し処理が遅くても問題ない
しかし、Callable CloudFunctionsは本当に最終手段になるので、他の方法で代替できないかをなるべく考え込むようにしましょう。
解決策
firestoreへ素直に直接アクセスしましょう。
素直にできないのであれば下記2点をまず考えてみましょう。
- 非正規化など設計を変えることで実装できるか
- firestoreイベントでのCloudFunctionsで実装できるか
マルチプラットフォームで実装が重複するといったことであれば、下記2点を意識すれば実装コストを下げることができます。
- Repository層にfirestoreへのアクセスをまとめるようにする(UnitTestもついでにしやすくなる)
- 一度実装したプラットフォームでの書き方をコピーする
まとめ
4つのアンチパターンを紹介しました!
まとめると、
-
- Join Lover: データをJoinする → 非正規化やフィールドを追加しよう
-
- All Listener: 全データをListenする → Limitとページネーションをしよう
-
- All For One Search: 検索を頑張る → 外部サービス使おう
-
- Intensive API: データの取り出しにAPIを挟む → 手法を考えて、クライアント実装コストを下げる工夫をしよう
という感じです!
RDB脳から抜け出してNoSQLライフでお手軽サービスライフをおくりましょう!
また、より具体的な使い方でのアンチパターンについては下記記事にも記載されていたのでこちらもよかったらご参照ください。
最後に、ワンナイト人狼オンラインというゲームを作ってます!よかったら遊んでね!
他にもCameconやOffchaといったサービスも作ってるのでよかったら使ってね!
また、チームビルディングや技術顧問、Firebaseの設計やアドバイスといったお話も受け付けてますので御用の方は弊社までお問い合わせください。