CONCEPT
データ活用診断
データ基盤構築
採用情報
clock2020.05.11 09:55
SERVICE
home

WepアプリにおけるFirestoreの活用 (1)

AUTHOR :   ギックス

293
ギックス
WepアプリにおけるFirestoreの活用 (1)

Firestoreのサブコレクション活用にあたって

最近、弊チームの Web アプリケーション開発ではデプロイ先として 基本的に Firebase を選択しています。
それにはいくつかの理由がありますが、Firestore の存在が大きいです。
2019 年に GA を迎えたサービスですので、まだ導入事例をたくさん見かけるというほどではないですが、一度使い始めると離れ難い便利さがあります。
弊社プロダクトのトチカチでも稼働中ですので、その事例も含め Firestore の魅力について書いていきたいと思います。
一度に全ての機能詳細に触れることは難しいので、今回の記事の前半では私が便利だと感じているFirestoreの一部機能の概要を。後半ではFirestoreの主要機能の1つである「サブコレクション」について、活用にあたっての注意点を書いてみました。

Firestoreの概要

Firestore は NoSQL ドキュメント指向データベースと呼ばれています。アプリケーションが Firestore から取り出せるデータ構造はオブジェクトモデルになりますが、ドキュメント指向データベースではこのオブジェクトモデルがドキュメントにあたります。
このイメージが正しいかどうかはさておき、私の Firestore のドキュメントに対するイメージは、クライアントから直接アクセスできるセキュアでスケーラブルかつ扱いやすい巨大な JSON です。データ同士のリレーションをキーと値のペアでまとめて管理し、それらをネストして階層化することができます。
私にこうしたイメージを持たせたのは、Firestore に次のような機能があるからです。

1. サブコレクション

Firestore ではルートに並列でコレクションを並べて管理するだけでなく、各コレクションのドキュメント配下にサブコレクションを用意して、階層を掘り下げていくことが可能です。(最大で100レベルまで)
特定の役割や意味をもったグループを 1 つのコレクション配下にまとめてグルーピングすることができ、ツリー構造での直感的なデータモデリングが可能になっています。これは他のNoSQLにはないFirestoreならではの機能です(2020/5/11現在)。また、Firestore独自のセキュリティルールにより、サブコレクションがより柔軟に設計できる様になっています。取り扱いが難しい部分もありますが、自由度が高いからこそできることが多くあります。データモデルの設計がUXにもDX(Developer Experience)にもセキュリティにも大きく影響するため、ここがFirestoreの楽しくも難しい部分かなと思います。

なお、最初にJSONと近いデータ構造と書きましたが、もちろん異なる点はあります。コレクションとドキュメントを交互に持たせなければならなかったり、ドキュメント1件あたりのサイズが1MB以下に制限されていたり、扱えるデータ型も少し異なったりします。ただ、私の場合はそれらの違いで不便を感じたことはあまりありません。タイムスタンプや緯度経度情報はそのまま入力できる様になっていますし、リファレンスと呼ばれるFirestore内のパスを値として格納できるので、任意のオブジェクトにワープすることもできます。(ちょっと困ったのはドキュメント内で配列のネストができなかった時ですが、そうした状況ではサブコレクションを活用すべきでしょう)

ただし、JSONと同じノリでデータモデルを設計してしまうと、Firestoreのアンチパターンにハマることがあります。Firestoreの特性をちゃんと理解していれば必然的に避けられる問題ですが、私は開発初期に見事にハマりました。詳細は後述します。

2. クライアントからの直接アクセス

私は最初混乱したのですが、Firestore はクライアントからデータへの直接アクセスが可能です。
一般にアプリケーション開発で DB を組み込むとなれば、クライアントと DB を直接つなぐなんてことはなく、セキュアに守られたサーバーサイドの裏に隠蔽するのが普通でしょう。
クライアントとサーバーで通信を行う際、通信の情報を完全に守ることは難しいので、情報を保護する必要性が最も高い DB をセキュアなサーバーからのみアクセスできる状態にしておくのが安心安全な形です。
しかし Firestore では、「クライアントとサーバーで通信を行う際、その通信の情報を完全に守ることは難しい」 -> 「だからサーバーの裏に隠す必要がある」という問題を独自のセキュリティルールにより解決し、クライアントから直接 DB(Firestore)へアクセスすることを可能にしています。フロントエンドでの使い方に合わせてセキュリティレベルを調整することも可能です。(サブコレクションのおかげで)
フロントエンドで DB 管理下のデータが必要となった際も、フロントエンドのロジック内で Firestore SDK を操作してデータに直接アクセスすることができます。サーバーサイド API を別途用意してあげるなどはせずに、です。

3.サーバーレスかつスケーラブル

Firestore の利用を開始する上で、こちらがサーバーやインフラの準備をする必要はありません。サーバーの運用や保守について考えることも特にありません。もちろん前述のセキュリティルールは超重要ですが。
仮に大量のアクセスが想定される場合でも、事前にこちらで Firestore がそのアクセスに耐えうるように調整しておくことは特にありません。read/write の制限や従量課金による費用増大などがあるので、その辺の考慮は必要です。

アプリ開発において上記の様な状態が当たり前になると、少なくとも自分はこれらがない世界に戻るのはなかなか困難です。
なぜ BigQuery のデータをクライアントから直接取り出せないんだ?なんてことをふと思ったりもします。パラダイムシフトですね。
他にもトチカチでは、リアルタイム・リスナー機能のおかげで機械学習モデリングの結果をリアルタイムにレポートへ反映させることができていたり、Firestore様様です。

前置きが長くなりましたが、今回はFirestoreの代表的な機能であるサブコレクションによってどんなデータモデルが作成できるのか、活用する上での注意点を書いていきます。
クライアントから直接アクセスできると何が嬉しいのかや、リアルタイム・リスナーなどの機能を使ってどんなUXを実装できるのかは別途紹介の予定です。

Firestoreのサブコレクション活用における注意点

サブコレクションの概要は先に説明した通りですが、公式ではコレクションはドキュメントのコンテナと言われています。

コレクションのコンテナの中に、さらにコレクションをネストしていけるのがサブコレクションです。JSONでは表現できないですが、イメージとしては下記の様な感じでしょうか。

サブコレクションを使うとどんなデータモデルを実装できるのかを挟みつつ、データモデルを活用する上で注意すべきポイントを2つご紹介します。

サブコレクション活用にあたり注意すべき2ポイント

Firestore はスキーマレスなサービスになっており、自由度高くデータモデルを設計することができます。しかし、そこに落とし穴があります。
落とし穴にハマらないための、私が思うポイントは下記の2つです。

  1. 同一コレクション直下のドキュメントにバラバラのフィールドを持たせない
  2. コレクションはドキュメントの総称(コンテナ)。データモデルを設計する際、コレクションは変数として考えない。

この2つは注意点としては初歩だと思いますが、Firestoreの特性を踏まえたデータモデルを事前にちゃんと設計できていないと意外とハマります。
それぞれ説明していきます。

1. 同一コレクション直下のドキュメントにバラバラのフィールドを持たせない

このポイントは誰しもそれはそうだと思う部分かもしれません。いくらスキーマレスと言えど、同一階層のドキュメントに全く異なるフィールドを持たせるケースは稀でしょう。単純に管理しにくい、ということが容易に想像できます。
では管理しにくいという問題を除いて、なぜ Firestore ですべきでないかというと、リレーションに対するクエリとセキュリティルールの取り扱いが困難になるためです。
簡単に例を示します(コードは公式ドキュメントから拝借)

こちらのwhereを使ったクエリでは、citiesコレクション配下のドキュメントの中で、フィールドのcapitalTrueであるものに絞り込んでいます。
Firestoreのクエリの起点となるリファレンスオブジェクトの都合上、基本的には同一階層のドキュメントに一律でクエリの条件が適用されます。よって、citiesコレクション配下のドキュメントのフィールドがバラバラであった場合、この様に Firestore に用意されたクエリを使って欲しいデータを絞り込む・取り出す、ということが難しくなります。
セキュリティルールについても同様です。セキュリティルールでは特定のフィールドの値を使ってセキュリティの条件を記述することができます。セキュリティルールは同一階層では一律になるため、異なるフィールドが混在しているとセキュリティに穴が空く可能性が高まります。

サブコレクションを使い始めると、様々な階層のドキュメントを管理し、クエリやリファレンスを使ってアクセスすることになります。それらの管理負担を軽減するには、スキーマレスといえども最低限のルールが必要になります。
慣れてくると、共通のフィールドを持たせつつドキュメントによっては一部異なるフィールドを入れるということはあります。スキーマレスのメリットですね。

続いて2つ目のポイントです。

2.コレクションはドキュメントの総称。データモデルを設計する際、コレクションは変数として考えない

このポイントは自分が初期に、それこそFirestoreをJSONライクに扱おうとしてしまい気づいたポイントです。私は無意識に、コレクションとドキュメントを異なるオブジェクトとして捉えたデータモデルを設計してしまっていました。
このポイントを押さえないと、Firestoreの一部機能を全く活かせないデータモデルになってしまうため、私的アンチパターンです。ただ当たり前すぎることなのか、公式などには特に記載がありません。(前述の通り、Firestoreの特性を理解していれば必然的に避ける設計ではあります)

弊社プロダクトのトチカチのデータモデルを少し簡単にして説明します。
トチカチでは開発初期に下記の様なデータモデルがありました。
当時はFirestoreもデータモデル設計も初めてで、「あまりネスト深くしたくないからとりあえずこれにするか〜」くらいの考えでした。

このモデルは、「あるユーザーはどのエリアの何月分のデータを購買したか」という状態を表そうとしています。サブコレクションである{PurchaseArea}にはエリアID(変数)が入り、その配下のドキュメントIDには年月(変数)が入ります。
イメージにすると下記の様な感じです。

ユーザーは複数のエリアを購買しますし、同じエリアについて異なる月のデータを購買します。
よって、ユーザーが新しいエリアを購買するたびに「サブコレクションである PurchaseAreaId-Aが増える」「PurchaseAreaId-A配下の yyyymm が増える」という状態になります。
そして上記の状態が私的アンチパターンです。詳しく説明します。

先に自分の中での答えを書いておくと、

  • コレクション(サブコレクション)は新しいデータモデルが誕生しない限り増えない(上記例で言えば、ユーザーの購買などによる状態変化によってコレクションが増えてはいけない)

よって、正解のデータモデルは

となります。イメージは下記の通りです。


先に書いた通り、コレクションとはコンテナでありドキュメントの総称です。ドキュメントがもつフィールド群がなんであるかを示す名称であり固定値です。
最初に例として載せたNG モデルでは、サブコレクション名は変数になっていました。

これで何が困るかというと、まずクライアントは SDK を使ってサブコレクションPurchaseAreareaIdの一覧を取得することができません。クライアント SDK にはコレクションをリストアップするメソッドは用意されていないからです。
よって、変数コレクションであるPurchaseAreaId配下のドキュメントを取得するパスを指定するには、ハードなPurchaseAreaIdをなんらかの方法で事前把握しておく必要があります。変数としてパターンが増加する可能性があるにもかかわらずです。
(サーバーサイドのSDKにはコレクションリストを取得するメソッドが存在するのですが、クライアントに存在しない時点で、Firestoreが1つ目の様なデータモデルを想定していないことが窺い知れます)

userIdドキュメントのフィールドにPurchaseAreaIdのリストやリファレンスを持たせて参照するという手段はあります。しかしこの場合、新しいコレクションが作成されるたびに userId ドキュメントをupdateしないと整合性が取れなくなりますし、特定のサブコレクションにアクセスしたい場合、毎度userId ドキュメントを読み取る必要があります。また、たくさんのエリアが購買される場合、そのuserIdのフィールドはどんどん肥大化していきます(ドキュメントのサイズ上限は1MB)。
サーバーサイド API にコレクションをリストアップしてもらって、それを受け取る方法もありますが、userId ドキュメントを読み取るのと同じく、そうした操作が必要になった時点で設計を見直すべきでしょう。本記事では触れませんが、UIに描画するために必要なFirestoreデータの読み取りは原則クライアントから行うべきだからです。

また、これが一番重要ですが、Firestore のセキュリティルールでは変数であるPurchaseAreaIdに対してセキュリティルールを抽象的に設定することができません。公式ドキュメントを読んでいただくのが良いですが、セキュリティルールを記述する際、ドキュメント ID には変数を設定できるのに対し、コレクション(サブコレクション)には変数を設定できず、ハードに値を書いてあげる必要があるからです。

つまり変数をコレクション名に適用した場合、変数によって新たなコレクションが生成されるたび、セキュリティルールにも変数の値をハードに記述しなければ、そのコレクションに個別のセキュリティルールを適用することはできません。変数のパターンが 100 個あれば、100 個のルールを書いてあげる必要があります。セキュリティルールが必要ないコレクションであればその限りではないものの、これは設計の欠陥以外の何者でもないでしょう。。
一応サブコレクションであれば、その親のコレクション(サブコレクション)と共通のセキュリティルールを適用することは可能ですので、完全な無秩序になる訳ではありませんが、技術的負債の芽となることは確実です。

ちなみに変数をコレクションに設定した時点で、私が描いていたデータモデルとは異なるデータモデルを定義してしまっていることがわかります。
最初のイメージ画像は少しミスリードしており、実際は下記の様なデータモデルになっています。

一度データモデルを定義したら、増加していくオブジェクトはドキュメントであり、コレクションはそのドキュメントの箱に過ぎません。もしドキュメントの箱が増えていく様なデータモデルを設計していたら、そのデータモデルは一度見直すことをオススメします。(使えない訳ではないんですが。。)

今回自分がアンチパターンと書いたデータモデルが有効なケースはあるのかなと少し(無理やり)考えましたが、

  • セキュリティルールを個別設定する必要がない
  • コレクション名をFirestoreのSDKを使って把握する必要がない
  • コレクションに持たせた意味(状態)にはフィールドが必要ない
  • Firestoreの仕様的に同一階層にコレクションがたくさん増えても問題ない(もしくはアプリの仕様上増えない

上記の様な条件を全て満たすのであれば、データ構造として不便を感じることはないのかもしれません。
が、機能拡張することは困難で柔軟性もない構造であることには変わりがないと思うので、わざわざこの構造する必要性もないでしょう。

以上の点を踏まえてコレクションとドキュメントを設計すると、必然的にコレクションの値は変数でなく固定値になると思います。よって先に書いた通り、アプリケーションにおいて新しいデータモデルが増えない限り、同じコレクション階層に新しいコレクションが作成されることはありません。

以上がFirestoreのサブコレクション活用にあたって、注意しておきたいポイントです。こちらはサブコレクションを使いこなすためのポイントというよりも、せっかくサブコレクションを取り入れたのに使い始めてから破綻してしまわないためのポイント、といった感じで捉えていただければと思います。


Go Horikoshi
MLOps Div. Lead / Kaggle Master
React.jsやTypeScriptを中心としたフロントエンド技術や、機械学習・データ分析・GISなどを活用したサービス開発について発信していきます。

SERVICE