firestoreをmigrationするcommand lineツール作った。後悔はしていない。

Gakuです。

就職決まりました。また東京舞い戻りです。
就業するとプライベート開発がはかどらなくなるので、なんとか今のうちに就業しつつ爆速で開発できる環境を整えようと奮闘しています。
僕の中で爆速開発のベースサービスと期待しまくっているfirestoreですが、NoSQLということもあり、なかなかスケールしにくいという印象を持っておりました。
Railsみたいに「migrationツールがあれば。。。」と考えていたんですが、なかったのでサクッと作ってみました。後悔はしていません。

# 作ろうとした経緯
@1amageek さんの「[[Qiita]Firebaseのマイグレーション](https://qiita.com/1amageek/items/41f8374a503d617b5b33)」を読んでいて、「migrationツールあればokじゃね？」と思ったので作りました。firestoreの思想とかは完全無視してるのでマサカリ怖いですが後悔はしていません。

# 作ったもの
npmで公開しています。まだあまり検証できていないし、ちょっとmigrationファイルの書き方をミスるとエラーが出る状態なので、ご利用される方は挙動を把握した上で慎重にご活用ください。(今後どんどん改良していきます。)
また、githubのissueいただければ、検討いたしますのでよろしくお願いします！(日本語でok！)
[[npm]firestore-migration](https://www.npmjs.com/package/firestore-migration)

# firestoreの勘所
## Field追加どうすん?
firestoreはテーブル定義等を行う必要がなく、どんどんデータをぶち込める仕様です。なので、例えばField追加したいと思った際は

```
user
|-user1{name:'gaku1', age:27}
|-user2{name:'gaku2', age:30, location: 'nara'}
|-user3{name:'gaku3', age:35}
```
こんな感じで、データの歯抜けが起こってしまいます。
こういう状態になってしまうと、「データがない場合は、こう表示する!」という処理をフロントで記述する必要があり、ちょっと面倒です。
あとRDB慣れていると気持ち悪いです(; ･`д･´)

## そこでfirestore-migration
firestore-migrationで全documentにfieldの追加を行うことができるように実装しています。

```
{
   "method": "ADD",
   "collection": "user",
   "params": [
      {"name": "location", "value": ""}
   ]
}
```
みたいにmigrationファイルを定義し、「fsmigrate -m .」を実施すると、

```
user
|-user1{name:'gaku1', age:27, location: ''}
|-user2{name:'gaku2', age:30, location: 'nara'}
|-user3{name:'gaku3', age:35, location: ''}
```
で全documentにFieldを生やしてくれます。やったぜ(*´ω｀*)！

## fieldの削除どうすんの？
firestoreでは一度格納したデータのFieldを削除することは基本的にできません。
なので、age field[number]を削除して、tosi field[string]にしたいな～っとおもった際、

```
user
|-user1{name:'gaku1', age:27, location: '', tosi: '27'}
|-user2{name:'gaku2', age:30, location: 'nara', tosi: '30'}
|-user3{name:'gaku3', age:35, location: '', tosi: '35'}
```
と、age fieldを放置してtosiを生やす形で対応します。これでもいいのですが、キモイので、できればage削除したいですよね？

## そこでfirestore-migration
Fieldを削除する際はこんな感じでmigrationファイルを定義します。

```
{
   "method": "DEL",
   "collection": "user",
   "params": [
      {"name": "age"},
   ]
}
```
これを実行すると、

```
user
|-user1{name:'gaku1', location: '', tosi: '27'}
|-user2{name:'gaku2', location: 'nara', tosi: '30'}
|-user3{name:'gaku3', location: '', tosi: '35'}
```
age fieldを削除することができます。やったぜ！

## 集計どうすんの？
firestoreのQueryは貧相です。なので、集計操作とかSQLだと簡単にできることが、firestoreでは基本的にできません。
なので「[[firebase]集約クエリ](https://firebase.google.com/docs/firestore/solutions/aggregation?hl=ja)」というテクニックを使って、集計データをfirestore上にデータとして保持した上で対応したりします。
ただ、この集約クエリ、「リリース後どうすんの？」って問題があります。これから新たに生成するデータに対しては集計処理をしてくれますが、これまでのデータに対しては集計処理を行うことができません。なので、頑張ってscriptを組んでデータ改変して対応する方法があるかと思うのですが、いちいちscript組むのも面倒ですし、リスキーです。

## そんな時にfirestore-migration
```
user
|-doc1{name: gaku}
|-doc2{name: gakuko}

purchese
|-pdoc1{uid: doc1, price: 500}
|-pdoc2{uid: doc2, price: 600}
|-pdoc3{uid: doc1, price: 700}
         ↓
user
|-doc1{name: gaku, priceall: 1200}
|-doc2{name: gakuko, priceall: 600}
```
こんな感じに集計データを生やしたい場合、

```
{
   "method": "AGG_COLLECTION",
   "collection": "user",
   "params": [
           {"name": "priceall", "aggCollection": "purchese", "if": "{uid} === $ID", "aggField": "price"}
   ]
}
```
これでok！全ドキュメントに集計したデータを格納してくれます。楽ちんちん。

# その他の機能
その他にも僕の中で「あったら楽なのにな～」っていう機能をいろいろ詰め込んでますので、詳しくはREADMEを見てもらえればと思います。
[[npm]firestore-migration](https://www.npmjs.com/package/firestore-migration)

# 注意点
## transaction処理について
全データを読み込んで、全データに書き込みという方法をとっています。できればtransactionを利用して適用という方法を取りたかったのですが、firestoreには1回のtransaction処理のオペレート数に上限500回という制限があるので、いろいろ考えた上でtransaction処理は行わず処理しています。
データの整合性は完全に無視していますが、今後何かしらの対応はしないといけないと思っています。

## データの読み書きについて
全データを読み込んで、全データに適用という方法をとっています。データ数が増えるとその分の利用回数が増えるので、「使用量」については十分注意っす。

## collecitonの読み取りについて
collectionの読み取りには最近実装されたcollectionGroup機能を活用して読み取りを行っています。
現状、collectionGroupは横断的な検索しかできないため、

```
user
|-doc1{name:gaku}-user
                   |-doc1{age:27}
```
みたいにcollection名がかぶると、すべてのデータに適用する仕様としています。
例えば、firestore-migrationのADDを適用すると

```
user
|-doc1{name:gaku, location:''}-user
                   |-doc1{age:27, location:''}
```
という挙動になってしまうわけです。ご注意ください。
(※ここはfirestoreのcollectionGroupがパスの制限機能を付けてくれることを期待しつつ、現状放置しています。)

# おわりに
まだまだ問題の多いツールですが、一旦「僕自身なら利用できるツール！」になったので記事書いてみました。
エラーハンドリングめんどいので放置している点、多数ありますが今後ぼちぼち対応していく予定です。

なんかほしい機能とか、「ここはこうした方が良い」とか「こういう処理でエラー出たから対応しろ糞野郎！」とかあったらissueくれれば大変うれしいです(*´ω｀*)
僕自身がどんどん活用していくためのツールなので、今後改良は続けていこうと思ってます。

それにしても「firestore楽しい。一人実装の友firestore!」

それではまた( ﾟДﾟ)ノシ