React + DRF + Firebase でアプリを作成する際の覚え書きその3(Firebase導入の前にFirebaseについて軽くまとめてみる)

# はじめに

FirebaseAuthentication を使いたいなと思ってとりあえず、公式のチュートリアル動画を見たら思いの外これまでの私のやってきたこととは違った新しい技術だったので軽くまとめないとこんがらがるなと思ったのでまとめてみます。

[公式によるチュートリアル動画](https://youtube.com/playlist?list=PLl-K7zZEsYLluG5MCVEzXAQ7ACZBCuZgZ)

## Firebase ってなに？ ということで軽くまとめてみる

`Backend as a service` あるいは `Mobile Backend as a service`に属するクラウドサービス。
所謂`BaaS/MBaaS`と呼ばれるようなサービスのこと。
主にモバイルアプリ開発において必要な機能を提供してくれる。
Google が買収したので Google Analytics を使え、以下のサービスを自分の開発するアプリに実装できる。

Firebase Cloud Messaging
→ ユーザー向けに通知やメッセージを行える機能。メーリングリストサービスのようなもの。
Firebase Authentication
→ 認証機能。メールアドレス・パスワードによる基本的なものから各種ソーシャルログイン、ゲスト認証もできる。
特に何もしなくても一応ユーザーのデータを保存してくれてる。

Firebase Realtime Database、Cloud Firestore
→
DB を提供してくれる。一応 Authentication の機能で保存されるユーザー情報もこちらの DB に保存される形になる。

Firebase Storage
→
アプリで写真や動画等コンテンツを作成する機能がある場合、それを保存したりアップロード・ダウンロードできるようにストレージを提供してくれるサービス。

Firebase Hosting
→
作ったアプリをデプロイできる。
ただし、モバイルアプリという前提なので静的コンテンツ+Firebase の組み合わせに限るようで、Django 等バックエンドをかませる場合はには使えない。

今回は Cloud Firestore について公式チュートリアル動画をさくっと見てきたのでそれを見た上でいくつかピックアップしてアウトプットしてみる。

## Firebase Realtime Database・Cloud Firestore について

どちらも NoSQL であり、かつリアルタイム性を持てる DB であり、こちらから制約をつけない限りはどんなデータ型でもすきにつっこめるという、既存の SQL での DB と比べるとかなり特異に感じるような DB になるようです。
中でも Cloud Firestore(以下 CF)の特徴としては

- 情報がツリー上かつ階層的に構造される
  → FRD もツリー構造ではあるが key-value によるそれであるのに対して、CF はドキュメント(例えばユーザー名・メールアドレス……といったユーザーの情報の集まり、つまり SQL における get()で取得できるデータのようなもの)に対してその集まりであるコレクション(SQL における Model に相当するようなもの)があり、さらにコレクションはドキュメントだけでなく、サブコレクションを持ちさらにそこにドキュメントを格納……といった階層的な構造にもすることができるという違いがあるようです。

これについて動画で説明されているのを聞いた限りですと

- SQL と違い関係テーブルかつオブジェクトの整ったデータに固執するわけではない。

- データ(ドキュメント)もデータ型に制限はなく、データベースレベルでの明確なルールは存在しない(データ型を統一したり、UserModel であるなら User に関するデータのみ入れるべき等々)

- 関係データベースではないので、SQL が存在せずリレーション関係からデータを取得したいなどといったクエリを投げられない。

といった特徴があります。
つまり、SQL では Model 内のあるデータのみにフィールドをつけたす……ということは Model に変更があった場合はマイグレーションを行い、既存のデータもそれに合わせてすべて更新されないと整合性が取れないのでエラーを出してしまいますが、CF においてはあるドキュメントだけに特定のフィールドを付け足す……といったことができますが、反面特定のデータセットが常に返されるとは限らないので取得するデータがこちらの意図したものであるかどうかのエラーハンドリングが必要であったり、SQL におけるリレーション関係でデータを絞り込むようなクエリは投げられないので、データの重複を是として、従来のリレーション関係からデータを引っ張ってくることが想定されるようなデータは 1 つのコレクションに入れておくことを検討しなければならないなど色々と難しいところもあるなと感じました。

動画ではレストランアプリを例に説明されていました。
以下に軽く過程をまとめてみます。

1. 各レストランの情報があるコレクションとそのレビューを管理するコレクションとがある。

2. レストランのレビューを取得するためにレビューコレクションにレストランの ID(name 等でも可)を仕込んでおけばそれをキーにレビューは取得できる。(SQL における外部キーでのリレーションするような手法)

3. ただし、SQL での外部キーでのリレーションとは違いいわゆる外部キーに当たる部分がオブジェクトではない(例えばユーザ ID を外部キーとする場合はそこにユーザーの情報を仕込んでおくことができるが、それができない)のでどのレストランのレビューなのかはこの手法で絞りこめるが、そのレビューをどのユーザーが書いたのかは取得できない。

4. なのでその場合は User コレクションのデータをそのままレストランコレクションにも保存するような手法を取らないといけない(データの重複・非正規化を是としている)

## なぜデータの重複・非正規化を是とするのか？

読み取りと書き込みのどちらに比重を置くかという観点。
例えばレストランのレビューは頻繁に書き込み・読み取りが行われるが、ユーザープロフィール(ユーザーの情報)は更新の頻度が少なく、読み取りに使われることが殆どである。
ならば 1 番ユーザーのアクセスが予測されるコレクションにデータを集約させることで、取得の際に複数のテーブルを跨ぐような取得をせずに済み、結果として読み込みが早くさせるのが合理的なのではないかという考え方のようです。

また、関係データベースであるとデータベースの肥大化にともない、より性能の高いマシンにデータベースを移行する必要があるが(垂直スケーリング)、NoSQL の場合は並列に複数のサーバーにデータベースを分散させることができるので(水平スケーリング)、よって CF のデータの絞り込みの基本は必要なデータを含むドキュメントに達するまで指定を繰り返すようなリクエスト文になる。(これは参照パスの指定でも代替可能)

## データ検索

ここが今までの SQL での DB と 1 番違うところです。
FRD も CF も原則部分的なデータの取得はできません。
つまり、クエリを出したらまるっと引っ張ってきてしまいます。
例えばレストランアプリであれば、そのレストランの場所の情報だけ取得して返すという処理を SQL であれば行えますが、Firebase の両 DB の場合は少なくともそのレストランに関する全ての情報を特に何もしていない場合すべて取得してしまう仕様ということです。
これはリアルタイム性持ち、かつ必要な情報はまとめておいたほうが最終的にアクセスする回数を減らせてパフォーマンスも上がるという考え方のようで、つまりいちいち個別にデータの一部を取得するより、最初に一括で取得して必要になったらそこから取り出す方がいいでしょう？ ということみたいですね。
ただ、これは反面難しいところもあり、例えばリアルタイム性をもたせた場合データの更新があった場合はその際逐次
DB は更新を行い、フロントがそれを拾って最新の状態に描画するわけですが、先程書いたとおり両 DB はツリー構造なので、ツリーの下層にあるデータが更新された場合は芋づる式に上層のデータも更新されることになります。
つまり、何も考えていないと意図しない描画が大量に発生してしまう恐ろしいアプリになってしまうことになります。
特に FRD の場合はその危険性は高いようです。
CF の場合はそれを防ぐため、取得できる情報はそのドキュメント自身のデータのみでありその下層に位置するであろうサブコレクションやそのドキュメントは取得できないように制約がかかっています。

つまり、

レストランコレクション
↓
各レストランのドキュメント、レビューコレクション(レストランコレクションのサブコレクション)

といった構造になっていた場合、レストランのドキュメントを取得する際にはレビューに関する情報は取得されないということになります。
なのでここでレビューの情報が欲しい場合は前述の通りさらにリクエストを投げる必要があるということになります。

### コレクションを跨いだデータ検索は可能

例えばレストランのレビューで Rate4.5 以上のレストランに filter したいなんてことはよくあると思います。
その場合 CF では以下のような過程を踏むようです。

- どのコレクションのどの field で検索したいかを指定する

→
例えば、Restaurant コレクションのサブコレクションである Review コレクションにおいて Rating という field で絞り込みをかけたいとする。
その場合、Review コレクションのドキュメントを横断して検索したいということになるので、指定するコレクションは Review であり、指定する field は rating ということになる。
そのようにクエリ文を書くと、エラーが出て index を作れという風に言われ指示通りにすると対象の field に index(絞り込むためのクエリ文のようなもの)が作成されるのでそれをそのままコピペすればいいという寸法です。
ちなみに index は自分でも設定できますが、間違って設定すると大変なのでできるだけ Firebase に任せることを推奨されています。
例えば同じ名前のコレクションがあるとそのコレクションを含んだ index を作ってしまいます。
なのでコレクションの名前は一意にするように推奨されています。
また、index の制限は 200 までなので無闇に追加はできません。
先程の例でアレ？ レビューコレクションは各レストランのドキュメントに対してのサブコレクションになるのでは？と思われた方もいると思いますが、レストランのサブコレクションにしていないとこのように Filter できないからですね。
なので、レビューコレクションには誰が、いつ、どのようなレビューを、どのレストランに対して、どのレートで書いたかというドキュメントが詰まっていることになりそのうちレストラン名がレストランコレクション(とその下層にあるレストランドキュメント)と重複してしまっていることがわかります。

また、index は異なるデータ型でのフィールドでは作成しないことを推奨されています。
要はコレクションを跨いだクエリを投げる場合は一気に、場所(str 型のフィールド)かつレート(int 型のフィールド)で Filter するみたいなことはせずに、どちらかで絞ってからさらにリクエストを行えということになります。

また OR 検索はできず、AND 検索のみ可能でいわゆる != で検索するということもできません。
OR 検索をしたいならそれに類するフィールドを設定しておく必要がある。
→ ex. イタリア料理とフランス料理のレストランを検索したいならそのどちらも内包するヨーロッパ料理のフィールドを作っておく等。

## その他大事だなと感じること

以下に CF でのクエリ文の投げ方の例をまとめておきます。

ー読み取り・書き込み・削除それぞれの処理ごとに課金対象となる。

→
例えば読み取りの場合だと、データを取得した件数 1 件ごとに課金なのでページネーションをつけないと大変なことになります。
これはここまで読めばわかると思いますが、改めてになりますが CF は一部のドキュメントのみ取得はできず、あるレストランのドキュメントがほしいとなったらコレクション下にある対象のドキュメントまでのデータすべてを取得してしまいます。
極端に言えば、100 件中 70 件目に目的のデータがあった場合自動的に 70 件のデータを取得してしまうということになります。
さらにリアルタイム更新ですでに取得しているデータに対して更新が入りそれを反映した場合も課金対象となります。
つまり、株チャートなどのアプリを作った場合、リアルタイムでチャートの変更を反映せねばならないので必然的に更新間隔によって課金額は高くついてしまいます。
さらにこれらはアクティブユーザーが増えれば当然ネズミ講の要領で増えていくので維持費は結構しんどくなってしまいます。
なので CF でのデータの取得は以下のように必ずページネーションを前提としたようなクエリ文を投げることになります。

```jsx

// ex. 1. 基本形
nextBatch = restaurantRef
    // whereFieldで絞り込みをかけていく
    // cityフィールドがTokyoのドキュメントを取得
    .whereField("city", isEqualTo:"Tokyo")
    //さらにそこから categoryフィールドがtempuraのドキュメントを抽出
    .whereField("category", isEqualTo:"tempura")
    // 抽出したデータをratingフィールドで降順に
    .order(by: "rating", descending: true)
    // 上位20件だけ取得
    .limit(to: 20)

// ex. 2. FireStoreの考え方の基本

nextBatch = restaurantRef
    .whereField("city", isEqualTo:"Tokyo")
    .whereField("category", isEqualTo:"tempura")
    .order(by: "rating"m descending: true)
    .limit(to: 20)
    // 21件目以降から～など続きから取得するためにどの位置から取得を始めるのか具体的に指示する
    .start(after: ["Tokyo", "tempura", 4.9])

// ex. 3. 2を短くする

// 大本のクエリ
nextBatch = restaurantRef
    .whereField("city", isEqualTo:"Tokyo")
    .whereField("category", isEqualTo:"tempura")
    .order(by: "rating"m descending: true)
    .limit(to: 20)

// nextBatchクエリで取得したところからTokyo・tempura・Rate4.9のレストランを取得
nextQuery = nextBatch.start(after: ["Tokyo", "tempura", 4.9])

// ex. 4.
// 2や3ではリアルタイム性をもたせた場合、データの更新が入ると取得漏れや正しい想定した位置から
// 取得できなかったり、エラーになるので改善する
// 必ず直前のクエリで取得したドキュメントを判断するように指定する
nextQuery = nextBatch.start(after: previousDoc)


```

つまり nextBatch クエリを投げたあと続きのデータ(21 件目以降)が欲しい場合は 20 件ごとに nextQuery を投げるということになります。
言わずもがなですが、当然クライアント側もページネーションができるように React 等を書かないといけません。

- 追加できるフィールドは 2 万まで

2 万フィールドも追加するなんてことは多分ないと思うんですけど、それとは関係なしにフィールドを多くすることは書き込み処理(及び更新処理)でのパフォーマンスに影響があるので非推奨のようです。
また、Map 型のデータを格納している場合、当然そのフィールドはマップフィールドとなりますがその場合、その Map 型のデータが持つキーそれぞれもフィールドとしてカウントされます。
例えば

```jsx

{a: "bar", b: "qux", c: "foo"}

```

というオブジェクトデータを格納していた場合、Map フィールドの数は 3 ということになります。

- ドキュメントに格納できるデータは 1MB まで

画像や動画無理……ということになりますね。
そもそもそのあたりは FireStorage 使ってくれということなのかもしれません。

- 同一ドキュメントに対して秒/1 回の書き込み制限がある

つまり、リアルタイムで同じデータを複数人が編集するようなものだったり、、短期間に大量に読み書きが発生することが想定される作業には向かない DB となります。
動画では例としてグループで一つのキャンパスに絵を書くようなアプリケーションが挙げられています。
その際には FRD を使ってほしいと動画中でも言われていますね。
また、データへのアクセスを少なくすることが寛容なので当然ながら例えば React ならメモ化を適切に行ないと、無駄な描画が生まれそれをトリガーとして DB にアクセスしてしまう……ということがないようにしたいです。

- ドキュメントは一部のフィールドのみ取得はできない

これは前述した通りです。
必ずデータセットで取得することになります。

- セキュリティルールを作らないといけない

FRD や CF はこれまで見てきたとおり DB 側ではほとんどルールが決まってません。
よってこちらでドキュメントに対して誰がアクセスできるか、どのように取得するか、どのような書き込みを許可するか……etc 等のルールを決めないといけないとのことです。
セキュリティルールはそれを定義したものになります。
公式のチュートリアルでは以下のような例で示されています。

```php

service cloud.firestore {
  match /databases/{database}/documents {

        // Restaurants:レストランコレクションに対して以下のルールを適用する
        //   - Authenticated user can read/ 認証済ユーザーのみ書き込み可能
        //   - Authenticated user can create/update (for demo) / 認証済ユーザーのみ新規作成・更新可能
        //   - Validate updates / バリデータを更新
        //   - Deletes are not allowed / Deleteリクエストは無効
    match /restaurants/{restaurantId} {
      allow read, create: if request.auth != null;
      allow update: if request.auth != null
                    && request.resource.data.name == resource.data.name
      allow delete: if false;

      // Ratings:
      //   - Authenticated user can read
      //   - Authenticated user can create if userId matches
    　//    ユーザーIDがサインインしているユーザーと一致する場合にのみ評価を作成
      //   - Deletes and updates are not allowed　/ delete及びupdateリクエストは無効
      match /ratings/{ratingId} {
        allow read: if request.auth != null;
        allow create: if request.auth != null
                      && request.resource.data.userId == request.auth.uid;
        allow update, delete: if false;

        }
    }
  }
}

```

ドキュメントは一部のデータを取得できないのと同じ理屈で、ドキュメント内の特定のフィールドやサブコレクションをPrivateなデータにすることはできません。
これはセキュリティールールでも定義はできないので、Privateなアクセスにしたいドキュメントはそれだけを集めたコレクションにまとめるようにしてくださいとのことです。

- queryが浅い

部分的にドキュメントを取得するのが不可能であることと合わせて、CFはドキュメントのサブコレクションは取得しないので、コレクションの階層を深くすればするほど(サブコレクションを作る等)目的のデータの取得が複雑になる(クエリ文をいくつも書かないといけない)り、また読み取りの回数も階層の数だけ増えてしまいます。
なので、常に取得が必要だと思われるデータはなるべくトップレベルであったり上の階層に保持しておくことが推奨されています。
同じ理屈で例えばお気に入り機能(今回の例だと各ユーザーがお気に入り登録したレストランの情報)などに使う情報はUserコレクションに持たせることが推奨されています(当然レストランコレクションとデータの重複が発生する)
また、必要であるならFavariteフィールドはレストランIDの配列にし、お気に入りの詳細(レストランの情報)などはSnippetとしてサブコレクションに保存してほしいようです。

## 終わりに

軽くチュートリアルの紹介動画を見ただけですが慣れるまではもちろんちゃんと使うにしても構築はかなり難しそうだなぁという印象を受けました。
今回自分がFirebaseで触るのは次回で使うFirebaseAuthenticationのみなので一旦このあたりで学習は止めますが、モバイルアプリケーションを作るなら今後は避けては通れないでしょうし、その時改めて腹を括って向かい合いたいなと思います。

## 参考

[Firebase Realtime DatabaseとFirestoreを使い分けていこうなという話](https://techblog.kayac.com/rtdb-vs-firestore)
[2021年度最新版クラウドサービスXaaS(Baas, Caas, Daas, Iaas, IDaas, IPaas, Paas, Saas, Faas)についてのまとめ](https://qiita.com/cocoa-maemae/items/6d9b6edc3082a678ed0f)
[Cloud Firestoreを実践投入するにあたって考えたこと](https://qiita.com/1amageek/items/d606dcee9fbcf21eeec6)
[【Firebase】Cloud Firestoreのデータ構造の決め方をFirebaseの動画から学ぶ](https://qiita.com/shiz/items/5f4c8ae19083ccdd46b2)