uwu

プログラミングの備忘録を書いています。誰かの為になれば幸いです

Firestore Cloud firestoreの公式動画まとめ5

前回の続きです!
前回の動画ではデータ構造の心構えや、Map、Array、サブコレクションの違いを見ていきました。
今回は「どのようにデータを構造すればよいか」についてのお話です。


www.youtube.com
※この記事で使われている全ての画像はこの動画内から切り抜いたものです。


また、レストランのレビューアプリを想定してお話していきます。



そのレストランレビューアプリでは検索ページや、
ボストンにあるTOP20の日本食レストランが検索できます。


そして、検索結果をクリックするとレストランの詳細が見れます。


レストランの詳細ページでは、そのレストランの情報や最新のレビュー数件が見れます。
そして、レビューをクリックするとレビューの詳細を見ることができ、他のレビューをもっと見るボタンを押すと全てのレビューが見れます。



こんな感じです。




データをどう管理するのがいいか?

今回の例ではレビューデータをどこに保管するべきかに焦点を当てています。
いくつかの方法を見ていきましょう。


方法1.レビューデータをマップや配列で管理する

レストランコレクションドキュメントの中にマップや配列を使ってレビューを保管する。





この方法では、ユーザーがレストランの詳細ページをクリックした時点で
全てのドキュメントのデータが読み込まれるのですぐにレビューデータを渡すことができます。
そして、サブコレクションに分けていないので読み取り回数も増えません。



この方法の問題は、レストランのドキュメントにレビューをマップで保存すると
もしユーザーがTOP20の日本食レストランを探したいときに、レストランの情報だけではなく
不必要なレビューの情報も同時に取得することになることです。



そして、もしレストランのレビューが増えすぎて、
1メガを超えたor2000フィールドのリミットに達した時に困ります。


さらに、最新10件のレビューを取得したいときにクエリでフィルターをかけることが
できないのでクライエントで処理する必要があります。


以上の問題点から、この方法はあまり良い案ではないかもしれません。

方法2.レビューデータをコレクションとして管理する

レストランコレクションと同じtop-levelのコレクションとしてレビューを保管する。





この方法では、レビューのデータをルートコレクションに置くことで「特定のレストランのレビューの取得」だけではなく、
「特定のユーザーが書いたレビュー一覧を取得する」みたいなことも簡単にできます。
そしてどれだけデータ量があったとしてもクエリが高速に動いて取得してくれます。


この方法の問題は、レビューを最新順に並べて取得したり、レビューの評価順に並べたいときに
複合クエリが必要になることです。





もしサブコレクションにそれぞれのレストランに対するレビューを保存していれば、複合クエリは必要ありません。



方法3.レビューデータをサブコレクションとして管理する



この方法の問題は、読み取り回数が増えることです。
これを回避するための案として、レストラン詳細ページでよく読み込まれるであろう
「最新10件のレビューの一部」などをレストランドキュメントに保存するのもよさそうです。


もしユーザーがあるレビューを詳しくみたい、全文を読みたいと思い
そのレビューをクリックしたときには、レビュードキュメントを読み込めばいいだけです。



このようなユースケースやアプリのコアバリューも考慮しながら、
どのような DB 設計をするか、Cloud Functions をどう使うかなどを、
開発者である皆さん自身が考えて決めていってください。



真偽値の保管方法

例えば、レストランが予約可能か?団体での飲利用は可能か?
などの真偽値はどのように保管すべきかを見ていきましょう。




ドキュメントにその値を直接保存する






Mapフィールドに項目名と真偽値のkey:value;ペアとして保存する





ドキュメントの真偽値フィールド(attributes)に配列として保存する





ただし、複数のarray_containsでクエリを発行できないので、
「子どもも楽しめて、予約 OK のレストランを探す」といった必要があるなら、フィールドを分ける方がいいです。


ユーザーのアクセス権限を保管する


例えば、レストランレビューアプリに「編集者」がいて、
編集者はレストランのドキュメントを編集できるとします。


このような場合には、「誰にその編集の権限を与えているか」を
userIDなどで管理・保存する必要があります。





セキュリティルールの詳しい説明は次の動画でしていきますが、この動画で知ってほしいのは
セキュリティルールによって、特定のドキュメントにアクセスできるユーザーを制御できるということです。



このように編集できる人をeditorsという配列にユーザーIDをいれて管理することができます。




もっといい例としては、マップを用いることです。
rolesというマップに、ユーザーID: ”役職”という形でデータを保存することで





役職が○○の時にのみ編集を許可する、といったセキュリティールールをつけることが可能です。





この良い例にも少し問題がありまして、Firestoreは情報の一部だけを読み取ることができないので
「どのユーザー ID がどのような権限をもっているのか」を検証するたびに全てのユーザーIDと役職を読み込んでしまうことです。



ユーザーIDはかなり不透明で、これをリバースエンジニアリングしても本当のユーザーが誰なのか、
解析することはできないですし、ユーザーIDはアプリごとにユニークですが
なんだかユーザー情報が漏洩しそうな気がしてセキュリティ面で不安が残ります。



そんなときは次の2通りのことをするといいです。


方法1.

編集権限をもつユーザーを、レストランドキュメントのサブコレクションの中に、
ドキュメントIDをそれぞれのユーザーIDに一致させて保存し、そのドキュメントの中にそのユーザーがもつ権限を記述しておく





方法2.

「プライベートデータ」とでも名付けたサブコレクションを作っておいて、
ここに「誰にどの権限があるか」という情報を持っておき、
このデータはパブリックにしないようにします。
このようなパブリックには公開しないコレクションは、
社内の営業チームだけが参照するような想定のデータを保存したり、
もしくは Cloud Functions がトリガーとして各種の API を実行するための
データとして使用するような場面で役に立ちます。




お気に入り機能

最後に「ユーザーがお気に入りのレストランを保存しておく機能はどうするか?」
という例をみていきましょう。



この場合、多対多のリレーションでありFirestore のような
NoSQLのDBでは度々問題になることがあります。



ひとつのアイディアとして、ユーザードキュメントの中に配列として
お気に入りのレストランを保存するとします。








この方法は、お気に入りのレストランを登録/外すといった操作が簡単に行えます。
アプリを起動するときに、このお気に入りリストに保存されたレストランIDの
一覧をメモリに保存しておけば、
レストラン一覧の中からレストラン ID が一致するものに、
ハートマークやスターマークを付けて、お気に入りのレストランを区別することもできそうです。



しかし、この方法だと、お気に入りのレストラン一覧をクエリで表示したいときに
お気に入りリストに保存されたレストランIDの一つ一つに対して、
別々にレストラン情報を読み込むクエリを発行する必要があり大変です。



お気に入りリストを頻繁に呼び出すわけではないならこの方法もありでしょう。



もし、お気に入りリストを頻繁に呼び出すなら「非正規化データ」を使うといいです。



つまり、単にお気に入りのレストランIDをユーザードキュメントに保存するだけではなく、
「お気に入りのレストラン一覧」を表示するために必要なデータを全てマップの中にいれてしまうということです。






そして、もしユーザーがレストランの情報をもっと詳細に見たいときには
そのレストランIDに対してクエリを発行する、という流れです。



この方法で注意しなければいけないことは、
もし以前お気に入り登録していたレストランが違うレストランになってしまった場合、
そのレストランをお気に入り登録していた全てのユーザーに対して
新しいレストランの情報を上書き保存しなければいけないことです。



その場合、こういったクエリを発行して全てのユーザーを抽出します。


全てのユーザーの中から、お気に入りフィールドID: rest_4215の
レストランデータの値が空文字 "" より大きいというクエリ





いろいろ説明してきましたが、もっとも良い例としては、
「favoriteRestaurans コレクションを作り、全ユーザーのお気に入りのレストランをそこに保存していく」
という方法です。




特定のユーザーがお気に入りにしたレストランを取得するクエリ

collection("favoriteRestaurans").where("user_id", "==", "user-1")


レストランで絞り込むクエリ

collection("favoriteRestaurans").where("rest_id", "==", "rest-1")

このようにクエリがとても簡単です。
また、レストラン情報が変更された時も、上のようなクエリを発行して抽出された
ドキュメントに対して、簡単に一括更新ができそうです。




6に続きます。