過去の自分に伝えたいFirestoreの失敗と勘違い

M2のmarutakuです。前回の記事は、@sh05_sh05による[FastAPIでの静的ファイル, index.htmlの設定(WebSocket Chatの例を直す)](https://qiita.com/sh05_sh05/items/cc37aa9188ec963c8fdd)でした。忙しい中、カレンダーが埋まってくれなかったからと急遽書いてくれました。ありがとう！
今回は「過去の僕に伝えたいFirestoreの設計と失敗」という題でお送りします。

一年前くらいから個人的にFirebaseを使う機会が増えてきました。特に、Firestoreはデータベースを立てなくてよくなるので、とても便利です(小並感)。しかし、NoSQLということもあり慣れるまでに様々な失敗をしてきました。ということで、この記事では過去に自分がした失敗とどうすれば回避できそうかを書いていきます。まだまだ経験が浅いので、間違えがあれば教えていただきたいです。
メソッドの名前などは、JavaScriptのSDKに準拠しています。

## そもそもFirestoreとは
[Firestore](https://firebase.google.com/docs/firestore?hl=ja)とは、Googleが提供しているモバイル、ウェブ、サーバー開発に対応したドキュメント指向データベースです。高機能なクエリ、リアルタイム性、無料枠の広さなどから色々な用途に用いられています。
FirebaseにはFirestore以外にも認証やストレージなどの機能も備わっているので、ぜひ使ってみてください。

## やらかした失敗、勘違い

### 1:Nのデータを配列で持つ
文字通り、1:Nのデータをドキュメントのフィールドで配列として持っていました。
例として、本のレビューサイトを考えてみてください。本のレビューサイトでは、本に対してレビューがたくさん紐づいています。

```json
# /books/:id
{
    "name": "...",
    "image": "..."
    "isbn": "...",
    "comments": [
        {
            "text": "...",
            "createdAt": "..."
        },
        ...
    ]
}
```
これをやると、単に本の一覧を持ってきたいだけなのに、全てのコメントを一緒に持ってきてしまいます。また、あとで出てくるセキュリティルールを細かく設定することもできなくなってしまいます。

解決策としては、`サブコレクション`を使うようにすることです。サブコレクションを用いることで、ドキュメントの下にさらにコレクションを作成することができます。先ほどの例であれば、サブコレクションを用いてこのようにすることがでます。

```json

# /books/:id
{
    "name": "...",
    "image": "..."
    "isbn": "...",
}
# /books/:id/comments/:comment_id
{
    "text": "...",
    "createdAt": "..."
}
```
コレクションをまたいでクエリがしたいときは、CollectionGroupを使用して実現できます。

### Firestore内でjoinしたい
RDBでは、inner joinやleft joinを用いてデータを一回のSQL文で全て持ってくることがあります。過去の自分は、Firestoreにも同じようにデータをjoinするような機能があると思っていました。

結論から言うと、そのような機能は知る限りではありません。理由としては、Firestoreではデータの結合はクライアント側で行います(クライアントサイドジョイン)。RDBに慣れていた自分はN+1問題などの理由から効率が悪そうに思いましたが、そもそもRDBではないのでそんなものかと言う感じです。


### 何でもかんでもリアルタイムアップデートを入手
Firestoreの特徴の一つにリアルタイム性があります。これは、Firestoreでデータの追加・更新・削除があった場合に、クライアント側に変更があったことを通知する仕組みです。自分は、Firestoreからデータを持ってくるときはリアルタイムアップデート受け取る必要があると思って、全てのデータを取得する箇所でリアルタイムアップデートを受け取っていました。

当然、Firestoreにはその時点であるデータを取得してくる機能が用意されています。[CollectionReference](https://firebase.google.com/docs/reference/js/firebase.firestore.CollectionReference)と[DocumentReference](https://firebase.google.com/docs/reference/js/firebase.firestore.DocumentReference)の両方に`.get()`メソッドが存在していて、これを用いることで一度だけデータを取得することができます。


### トランザクションが無いと思ってた
FirestoreにはRDBのようなトランザクションはないと思っていました。もちろんFirestoreにはトランザクションを実現するための機能が用意されています。
以下のコードでトランザクションを実現できます

```javascript
const store = firebase.firestore()
const documentRef = store.collection("hoge").get("fuga")
await store.runTransaction(async (transaction) => {
    const doc = await transaction.get(documentRef)
    const vote = doc.data().vote + 1
    transaction.update(documentRef, { vote });
}).catch((e) => console.log("Error: " + e))
```

### セキュリティ弱い
Firestoreはクライアント側でリクエストを送ったりしているので、なんやかんやでハックすればデータベースの中身を自由に覗かれてしまうと思い込んでいました。しかし、Firestoreにはセキュリティルールというセキュリティを設定する機能が存在しています。セキュリティルールを用いるとユーザごとにアクセスできるドキュメントを制限できたり、Firebase Authenticationを使ってログインしているかどうかでアクセスできる範囲を変えたりできます。

テストモードでは、指定された日にちまでは誰でも無制限にアクセスができてしまうので注意が必要です。

## 終わりに
過去に自分が行なった失敗や勘違いをまとめました。この記事を見て同じミスをしなくなる人がいれば幸いです。
明日もFirebase関連の記事を書く予定です。


## 参考にしたサイトなど
* [Cloud Firestoreを実践投入するにあたって考えたこと](https://qiita.com/1amageek/items/d606dcee9fbcf21eeec6)
    * とても参考になる。この記事読むより圧倒的に参考になるから今すぐ読むべき。
* [Firestore で、DB設計を考える際に参考になった情報](https://qiita.com/samuraikun/items/dfe7d1081f62359b0dcd)
* [Cloud Firestore のベスト プラクティス](https://firebase.google.com/docs/firestore/best-practices?hl=ja)