Cloud Firestoreに追加されたFieldValue.increment()は期待以上！！

どうも[@1amageek](https://twitter.com/1amageek)です。

先日のFirebase SDKのアップデートで追加された`FieldValue.increment()`が期待以上の仕様変更だったので紹介します。

## FieldValueとは？

開発においてサーバーに処理を任せる必要がある要件が存在します。
例えば、日付の管理などがそれに当たります。クライアント側では日付の変更がデバイス毎に可能なため正確な日付を取得できません。そういった場合はサーバー側で__Stampstamp__を取得してDBに保存するはずです。__Cloud Firestore__では__FieldValue__を使うことでこれを簡単に行なってくれます。

実は__FieldValue__はCloudFirestoreの前身とも言えるFirebase Realtime Database（RTDB）の時から存在し、RTDBでは`ServerValue`という名前で提供されていました。Cloud Firestoreでは`FieldValue`として提供されています。

__FieldValue__を使うことで配列をコントロールやFieldを削除など便利な仕様が含まれているので、目を通しておくといいと思います。

- [firebase.database.ServerValue](https://firebase.google.com/docs/reference/js/firebase.database.ServerValue)
- [firebase.firestore.FieldValue](https://firebase.google.com/docs/reference/js/firebase.firestore.FieldValue)

## FieldValue.increment

今回の変更点のもっとも重要なポイント!

__FieldValue.incrementの考案者は僕であることですw😎__
[FR: ServerValue.increment, ServerValue.decrement #257](https://github.com/firebase/firebase-ios-sdk/issues/257)
<img width="1013" alt="スクリーンショット 2019-03-21 20.39.47.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/6094402d-1c4f-9bb4-a679-6e30b0f3c4e7.png">

※ すみません。あまりに嬉しかったのでつい🙇‍♂️

ちょうどCloud Firestoreがリリースされた直後くらいにRTDBへFRをしており、当時からCloudFirestoreの仕様として導入を計画してくれていたので今回本当に実装されて本当に嬉しい限りです。

さて、そんなことよりも今回の仕様変更の説明、そしてどこが期待以上なのかを解説していきたいと思います。

### FieldValue.incrementでできること

その名前の通りFieldValue.incrementではインクリメント処理を行います。通常Cloud Firestoreでインクリメントを行う場合は必ず__トランザクション処理__が必要になります。

なぜ？っと思った方はこちらの記事を読んで合わせて読んで頂けると理解が深まります。
[Firebase Cloud FirestoreのTransactionについて考える](https://qiita.com/1amageek/items/2eff436fb69bea5875ea)

先ほどの[こちらのGithubのissue](https://github.com/firebase/firebase-ios-sdk/issues/257)にも記載していますが、Firebaseでクライアントから__トランザクション__をする処理は少し大げさになります。


__TransactionでIncrementする__

```swift
        let user: User = User(id: "hoge")
        (0..<100).forEach({ (index) in
            Firestore.firestore().runTransaction({ (transaction, errorPointer) -> Any? in
                let snapshot: DocumentSnapshot = try! transaction.getDocument(user.reference)
                let count: Int = snapshot.data()!["followersCount"] as? Int ?? 0
                transaction.setData(["followersCount": count + 1], forDocument: user.reference, merge: true)
                return nil
            }, completion: { (_, _) in })
        })
```

__FieldValueでIncrementする__

```swift
        let user: User = User(id: "hoge")
        (0..<100).forEach({ (index) in
            user.reference.setData(["followersCount": FieldValue.increment(1.0)], merge: true)
        })
```

スッキリかけますね。メリットはスッキリかけるという事だけではありません。

#### ~~ドキュメントへの最大書き込み速度を越えることが可能に~~

<img width="870" alt="スクリーンショット 2019-03-21 18.25.28.png" src="https://qiita-image-store.s3.amazonaws.com/0/80287/ae0c87d2-4bf6-1729-44f7-05a781b049d7.png">

~~Cloud Firestoreは`ドキュメントへの最大書き込み速度が1秒あたり１回`と言う制限を持っていました。
この制限により、上記のような連続的なトランザクションでは数回しか処理されず、実際上記のサンプルコードも全て処理されません。今回の`FieldValue.increment`の追加によりこの制限を超えてインクリメント処理を行うことが可能になりました。回数を増やして検証しても途中で中断されることはありませんでした。~~

~~つまり[分散カウンタ](https://firebase.google.com/docs/firestore/solutions/counters?hl=ja)が不要になったんです😭~~

ドキュメントより`最大書き込み速度が1秒あたり１回`は変わらないようですが、トランザクションのように途中中断されると言う訳ではなさそうです。


次の動画は30回のインクリメント処理を`FieldValue.increment`と`Transaction`を比較したものです。

![ezgif.com-resize.gif](https://qiita-image-store.s3.amazonaws.com/0/80287/674c5690-31c2-1f96-16e7-09ea6db8cd7d.gif)
(10秒くらいあります)

動画でも確認できますが、Transactionでは処理が途中で中断していることがわかります。6回くらいで処理が止まります。
`FieldValue.increment`の強力性がわかって頂けたでしょうか？

### FieldValue.incrementの注意点

次に、今回iOSのSDKで検証を行なってみた時の、注意点をご紹介します。

1. 連続的なインクリメント処理中はリアルタイムでドキュメント監視できない
2. 書き込み制限は緩和されたが高速ではない


#### 連続的なインクリメント処理中はリアルタイムでドキュメント監視できない

それでは、注意点について解説します。次の二つの動画をみてください。この二つは全く同じ処理をしていますが、監視の方法に差があります。

__すぐにViewに反映された__

![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.amazonaws.com/0/80287/0454dec8-90cd-2c4f-03fe-2c99197483ac.gif)

__DBの更新が最後まで行われた後にViewに反映された__

![ezgif.com-video-to-gif (1).gif](https://qiita-image-store.s3.amazonaws.com/0/80287/841fe1a6-b159-0603-fdf2-27e8136494ec.gif)

こちらが今回検証に使ったコードです。違いは`snapshot.metadata.hasPendingWrites `です。

```swift
        self.listener = User(id: "hoge").reference.addSnapshotListener(includeMetadataChanges: true) { [weak self] (snapshot, error) in
            if let error = error {
                print(error)
                return
            }
            if let snapshot = snapshot {

                // ローカルのデータを反映する
                if !snapshot.metadata.isFromCache {
                    if let data: [String: Any] = snapshot.data() {
                        let count: Int = data["followersCount"] as! Int
                        self?.label.text = "\(count)"
                    }
                }

                // ローカルのデータは無視する
                if !snapshot.metadata.isFromCache && !snapshot.metadata.hasPendingWrites {
                    if let data: [String: Any] = snapshot.data() {
                        let count: Int = data["followersCount"] as! Int
                        self?.label.text = "\(count)"
                    }
                }
            }
        }
```

__hasPendingWrites__が`true`であるときは、まだCloudFirestoreにデータが反映されていないことを意味します。__hasPendingWrites__が`false`の時のみViewを更新するように変更したところ、動画では最後の処理が終わるまでViewが更新されないことがわかります。

ただ、Web Consoleではリアルタイムに同期されてそうなので監視可能なのかもしれません。
ちょっとまだわからん。


ちなみ、分離されたDBで整合性が崩れた後、最終的に整合性が取れることを__結果整合性__と呼びます。
__結果整合性？__となった方はこちらの記事を合わせて読むと理解が深まります。

- [Firebase Realtime Databaseとはなんなのか？](https://qiita.com/1amageek/items/b350ee5ef0c9b2406583)
- [Firebase Cloud FirestoreのTransactionについて考える](https://qiita.com/1amageek/items/2eff436fb69bea5875ea)

#### 書き込み制限は緩和されたが高速ではない

上記の動画からも見て取れますがインクリメントが目視可能な速度で進むことも確認できます。1秒に1回までの制限は緩和されてそうですが、1秒に10回くらいまでが限度なのかもしれません。

正確仕様の制限や情報が記載されてないので、明言できませんが高速ではなさそうです。しかし処理を任せてしまえば途中で中断されることもないので応用範囲が広いことがわかってもらえたと思います。


## Transactionとの使い分け

インクリメント処理に関しては、ほぼ間違いなく`FeildValue.increment`を使う方がメリットが大きいと思います。在庫管理などの条件によっては処理が中止する必要がある場合には利用することが出来ないと思います。
やはり一番はフォローカウントくらいですかね🤔売り上げの計算もこれでいいかも

---

今後のFirebaseの改善にも大きく期待ですね👍🏻
今年は、FRだけでなくFirestoreのコントリビュータになれるよう頑張りたいなぁ😃

今回の検証にはこちらのライブラリを使ってます。ぜひ参考にしてください。
[Pring for iOS](https://github.com/1amageek/Pring)

Firebase専門の技術顧問をやっています。
お困りごとがあれば[@1amageek](https://twitter.com/1amageek)までDMをください✨

Firebaseの質問も受け付けてるもくもく会をweworkで定期開催してます。
ご参加をご希望の方はこちらまで
https://mokudev-tokyo.connpass.com/event/124305/

サーバーレス・Firebase専門のアプリ開発を行う事業をやってます。
新規サービスを高速で作りたい企業様はぜひ[Stamp Inc](https://stamp.team/)ご連絡ください。
https://stamp.team/