Firebaseで作ったWebサービスを3ヶ月運用してみて、ハマったこと・知っておきたかったこと

Nuxt.jsとFirebaseで作っていた[Webサービス](https://tsundoku.site/)を7月末にリリースして、はや3ヶ月。。
RDB脳なのでFirebaseを使った開発でいろいろとハマった。。そのポイントを整理してみました。

Firebaseをはじめようとしている人の一助になれば。

## Nuxt.js(SPA)+Firebaseで作っています！

以前、以下のような記事を書いたのですが、そのFirebase関連ぽいまとめです。
・[Nuxt.js(SPA)+Firebaseで積読用の読書管理サービスを作ってみたときにハマったこと... - Qiita](https://qiita.com/kira_puka/items/8c1d1240c3aa200cbec0)


前回同様、内容的にはドキュメントをよく読めば書いてあることばかりですが、
実際に運用したり、機能追加したりする時に、気づくので、手戻りが多く...
あらかじめ、知っていたら良かったなと思う点をまとめています。

Firebaseはとてもよいですが、RDBに慣れ親しんでいると、
思わぬところでハマったり、運用してみて初めて分かることが多い感じに。。

ハマったことが多いですが、**かなりコストを抑えて作れていてすごい( ﾟдﾟ)!**
というのが、Firebaseを使ってみた感想です♪

## 全体の構成はこんな感じ

<img width="845" alt="スクリーンショット 2019-08-13 23.06.17.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/3ab45cc9-0a03-5a5b-8297-a6f5af1e9c0d.png">

- SPAモードのNuxtアプリをFirebase Hositingにデプロイ
- データベースはFirestore
- OGP生成のためにCloud Function for Firebase+ImageMagick
- OGP用のHTML生成でCloud Function for Firebase
- 外部APIを利用したいのでZEIT NOW経由でのAPIを用意

## わたしはここでハマりました。。

### FirestoreでORがつかえない

DBを扱っているとORが使いたくなりますが、Firestoreでは使えません。。
公式ドキュメントの[ここ](https://firebase.google.com/docs/firestore/query-data/queries?hl=ja)に書いてある制約事項。。

> 論理 OR クエリ。この場合は、OR 条件ごとに独立したクエリを作成し、アプリでクエリ結果を結合する必要があります。

<img width="845" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/bab57b35-7a52-0d38-56b1-ca69d726b440.png"/>


シンプルなクエリだったら複数回呼べばよいだけなので、あまり問題ないのですが、
orderByやlimitと一緒に使うと困ります。。

開発している[積読ハウマッチ](https://tsundoku.site)だとこの部分。


<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/6f8885d4-6cab-1c29-aa66-d6bd0172ae8e.png" />


* 読書中→積読の順序で表示して、
* 各ステータスは更新日時の降順で表示し、
* 30件ずつで表示する

という感じの処理をしています。

ORが使えないので、

* 読書中を検索
* 結果が30件未満だったら、残り分、積読を検索

という感じでやっています。


### Firestoreで別フィールドでの範囲指定+orderByが使えない

これもFirestoreの制約事項。。

> 複数のフィールドに範囲フィルタを適用するクエリ（前のセクションの説明を参照）。

<img width="845" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/bab57b35-7a52-0d38-56b1-ca69d726b440.png"/>

<img width="845" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/b9bd6977-5bf9-a4b6-547d-a88ebf2ebb1c.png" />

公式ドキュメントだとこのあたり。
複数のorderByとかは使えるけど、where+orderByだとダメ。
・[Cloud Firestore で単純なクエリと複合クエリを実行する  |  Firebase](https://firebase.google.com/docs/firestore/query-data/queries#compound_queries)
・[Cloud Firestore でのデータの並べ替えと制限  |  Firebase](https://firebase.google.com/docs/firestore/query-data/order-limit-data?hl=ja)

たとえば、[積読ハウマッチ](https://tsundoku.site)だと

- 持っている人が10人以上の本を、
- 更新日時の降順で並び替える

とかができない。。

実際にやる場合には、

- あらかじめ、持っている人が10人以上の本を別コレクションとして作成しておいて、
- そのコレクションに対して、更新日時の降順で並び替える

という形で対応する必要がある。。

開発当初・リリース前では、あまり気にならない制約事項だったけど、
リリース後の機能追加などで制約に引っかかるように。。


### ドキュメントフィールドの削除は`FieldValue.delete()`

[公式ドキュメント](https://firebase.google.com/docs/firestore/manage-data/delete-data?hl=ja)にちゃんと書いてあるけど、
ドキュメントフィールドを削除したいときは、`FieldValue.delete()`を使わないといけない。。

雰囲気で使い始めたときは、`null`をセットすると削除できると思ってて、
そのまま`null`がセットされてた(*´ω｀*)

### 更新するときは、`update`と`set({merge: true})`がある

[公式ドキュメント](https://firebase.google.com/docs/firestore/manage-data/add-data?hl=ja)をよく読むと書いてあるけど、更新する方法は2種類ある。

1. `update`は、対象のドキュメントフィールドが無いとエラーになる
2. `set({merge: true})`は、ドキュメントフィールドがなくてもエラーにならない

公式ドキュメントの説明では、こんな感じ。

> ドキュメントが存在するかどうかわからない場合は、新しいデータを既存のドキュメントに結合するオプションを渡し、ドキュメント全体が上書きされないようにします。

たとえば、こんなドキュメントがあって、

```json
{
  name: "Alice",
  age: 20
}
```

年齢を30歳に変えたいなと思った場合、こんな感じ。

```javascript
const docRef = db.collection("user").doc("Alice");

// updateの場合、すべてのドキュメントフィールドを渡さないといけない
await docRef.update({ age: 30 })

// set({merge: true})の場合、変更したいドキュメントフィールドだけ渡せばOK
await docRef.set({ age: 30 }, { merge: true })
```

間違えて、マージなしの`set()`を使うと。。

```javascript
const docRef = db.collection("user").doc("Alice");
await docRef.set({ age: 30 })

// => 
// {
//   age: 30
// }
```

こんな感じで、`name: "Alice"`が消えてしまいます。。

その名の通り、マージなので、新しいドキュメントフィールドを追加したいときにも便利(*´ω｀*)

```javascript
const docRef = db.collection("user").doc("Alice");
await docRef.set({ bookNum: 0 }, { merge: true })

// => 
// {
//   name: "Alice",
//   age: 30,
//   bookNum: 0
// }
```

マイグレーションするときは、これを使って、フィールドを追加したりしている(*´ω｀*)


### トランザクション内では最大500件まで

公式ドキュメントだとこのあたり。
・[トランザクションと一括書き込み  |  Firebase](https://firebase.google.com/docs/firestore/manage-data/transactions?hl=ja)

制約がいくつか書かれているけど、以下のな感じ。

1. トランザクション内では、**書き込みよりも読み込みを先にしないといけない**
2. トランザクションが**最大リクエストサイズの10MiBまで**
3. 1度でできる書き込みは**最大500のドキュメントまで**

特に気になるのが、**書き込みは最大500のドキュメント**の部分。

何千件も更新しないといけない場合に、問題が発生してきます。。

5000件あると、10個のトランザクションに分ける必要がありますが、
たとえば、1000件目で失敗した場合、最初の500件のみ実行される状態でエラーに。。

そうなった場合、**どこでエラーが起きたかを記録する仕組みがないと大変なことに**。。

[積読ハウマッチ](https://tsundoku.site)では、ログなど既存の情報から復元することで対応可能でしたが、
**エラーが発生した場合、そのログを残しておくなどの仕組み**があるとよいなと。。

バグがないように作るのは大事なのですが、バグが起こったときに、
リカバリできる情報や手段を用意しておくのも同じく大事。。


### Firestoreトリガーは不整合を起こしやすい

Firestoreで変更があったら、Cloud Functionsを呼ぶCloud Firestoreトリガー。
特定パスのCRUDに応じて、処理を組み込めるので便利。
・[Cloud Firestore トリガー  |  Firebase](https://firebase.google.com/docs/functions/firestore-events?hl=ja)

ただ、トランザクションと同じ感じで、**トランザクションが分断されるので要注意**。。

クライアント側でトランザクションを使っていたとしても、
トリガーで起動するCloud Firestoreはトランザクションに含まれません。。

そのため、クライアント側で正常終了したと思っても、
Cloud Firestore側でエラーが起こると、全体として不整合が起こった状態になります。。

Firestoreトリガーを使うときは、
**トランザクションを分けてもよい処理なのか**をよく検討する必要があります。。

### Firestoreのスキーマ変更が大変

Firestoreはドキュメント型のNoSQLで、NoSQLはクエリ志向のDBです。
そのため、クエリが増える・変わるとスキーマを変更する必要が出てきます。

たとえば、「〇〇の総数」みたいなのはドキュメントフィールドとして値を持たせる必要があり、
あとでほしいと思った時に、マイグレーションが必要になってきます。。

Firestoreには[分散カウンタ](https://firebase.google.com/docs/firestore/solutions/counters?hl=ja)という便利なものがありますが、
> 次のコードは、分散カウンタを初期化します。
と記載されているとおり、値が0のドキュメントフィールドが必要だったり。。
    

なので、ローカルPCでfirebase-adminを実行して、
Firestoreのデータを変更する仕組みを用意しておく必要があります。

ローカルPCでfirebase-adminを実行するときの話はこちらの記事に。
・[ローカルPCからfirebase-adminを使ってFirestoreを操作する(管理ツール)](https://www.memory-lovers.blog/entry/2019/07/25/073000)
・[ローカルPCでfirebase-adminを使って一括削除/一括更新する - Qiita](https://qiita.com/kira_puka/items/e4d4846f394d36e29776)


ただ、ドキュメント数が1万とかだと、
**あっという間に無料枠を使い切るので、頻繁な変更は要注意**。。

(とはいえ、1日に2万件の読み込みが無料だったり、)
(10万件ごとに$0.18/20円くらいなので100万件でも200円くらい。)
(RDBで100万レコードを扱うよりも、安い気がしてきている)


## おわりに

わりとネガティブな内容になっている気もしますが、
制約を制約として認識していれば、問題ない話ばかりだと思ってます。

大変だったのは手探りでやっていたため、後で知ることになったため。。
設計とかする前に知っておきたかったまとめです。。

個人的にはFirebaseはとてもよく、
**かなりコストを抑えてサービスを公開できてすごい( ﾟдﾟ)!**
というのが、Firebaseを使ってみた感想です♪

気をつけることも多いですが、慣れれば大丈夫(*´ω｀*)
とはいえ、プロダクトによっては合わないこともあるので、なにかの参考になれば！

## こんなのつくってます!!

積読用の読書管理アプリ 『積読ハウマッチ』をリリースしました！
[積読ハウマッチ](https://tsundoku.site)は、Nuxt.js+Firebaseで開発してます!

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/478782/572d4947-f40b-e4dc-1c9c-bc584cd2a66c.png" width="200"/>

もしよかったら、遊んでみてくださいヽ(=´▽`=)ﾉ
機能追加が増えると知見も貯まるので、貯まったらまた記事を書こうと思います♪

要望・感想・アドバイスなどあれば、
公式アカウント([@MemoryLoverz](https://twitter.com/MemoryLoverz))や開発者([@kira_puka](https://twitter.com/kira_puka))まで♪