Firebase
CloudFirestore

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

どうも@1amageekです。

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


FieldValueとは?

開発においてサーバーに処理を任せる必要がある要件が存在します。

例えば、日付の管理などがそれに当たります。クライアント側では日付の変更がデバイス毎に可能なため正確な日付を取得できません。そういった場合はサーバー側でStampstampを取得してDBに保存するはずです。Cloud FirestoreではFieldValueを使うことでこれを簡単に行なってくれます。

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

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


FieldValue.increment

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

FieldValue.incrementの考案者は僕であることですw😎

FR: ServerValue.increment, ServerValue.decrement #257

スクリーンショット 2019-03-21 20.39.47.png

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

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

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


FieldValue.incrementでできること

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

なぜ?っと思った方はこちらの記事を読んで合わせて読んで頂けると理解が深まります。

Firebase Cloud FirestoreのTransactionについて考える

先ほどのこちらのGithubのissueにも記載していますが、Firebaseでクライアントからトランザクションをする処理は少し大げさになります。

TransactionでIncrementする

        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する

        let user: User = User(id: "hoge")

(0..<100).forEach({ (index) in
user.reference.setData(["followersCount": FieldValue.increment(1.0)], merge: true)
})

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


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

スクリーンショット 2019-03-21 18.25.28.png

Cloud Firestoreはドキュメントへの最大書き込み速度が1秒あたり1回と言う制限を持っていました。

この制限により、上記のような連続的なトランザクションでは数回しか処理されず、実際上記のサンプルコードも全て処理されません。今回のFieldValue.incrementの追加によりこの制限を超えてインクリメント処理を行うことが可能になりました。回数を増やして検証しても途中で中断されることはありませんでした。

つまり分散カウンタが不要になったんです😭

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

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

ezgif.com-resize.gif

(10秒くらいあります)

動画でも確認できますが、Transactionでは処理が途中で中断していることがわかります。6回くらいで処理が止まります。

FieldValue.incrementの強力性がわかって頂けたでしょうか?


FieldValue.incrementの注意点

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


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

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


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

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

すぐにViewに反映された

ezgif.com-video-to-gif.gif

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

ezgif.com-video-to-gif (1).gif

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

        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)"
}
}
}
}

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

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

ちょっとまだわからん。

ちなみ、分離されたDBで整合性が崩れた後、最終的に整合性が取れることを結果整合性と呼びます。

結果整合性?となった方はこちらの記事を合わせて読むと理解が深まります。


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

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

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


Transactionとの使い分け

インクリメント処理に関しては、ほぼ間違いなくFeildValue.incrementを使う方がメリットが大きいと思います。在庫管理などの条件によっては処理が中止する必要がある場合には利用することが出来ないと思います。

やはり一番はフォローカウントくらいですかね🤔売り上げの計算もこれでいいかも


今後のFirebaseの改善にも大きく期待ですね👍🏻

今年は、FRだけでなくFirestoreのコントリビュータになれるよう頑張りたいなぁ😃

今回の検証にはこちらのライブラリを使ってます。ぜひ参考にしてください。

Pring for iOS

Firebase専門の技術顧問をやっています。

お困りごとがあれば@1amageekまでDMをください✨

Firebaseの質問も受け付けてるもくもく会をweworkで定期開催してます。

ご参加をご希望の方はこちらまで

https://mokudev-tokyo.connpass.com/event/124305/

サーバーレス・Firebase専門のアプリ開発を行う事業をやってます。

新規サービスを高速で作りたい企業様はぜひStamp Incご連絡ください。

https://stamp.team/