Cloud Firestore を使う利点の一つとしてリアルタイムに変更を取得できる点が挙げられます。公式ドキュメントでは Go SDK はまだサポートされていないと書かれていますが(2018/12/5 時点)、機能自体は SDK に実装されています (v0.21.0 リリースノート)。本記事では、Firestore の Go SDK で Firestore のリアルタイムアップデートを取得する方法について述べます。

概要

リアルタイムタイムアップデートは、変更を監視したい対象の SnapshotIterator を取得することで得られます。

SnapshotIterator には下記の 2 種類があります。

DocumentSnapshotIterator でドキュメントのリアルタイムアップデートが、QuerySnapshotIterator でクエリ結果のリアルタイムアップデートが取得できます。

ドキュメントのリアルタイムアップデートを取得する

単一ドキュメントのリアルタイムアップデートは、下記のように DocumentReference.Snapshots から取得できます。DocumentSnapshotIterator.Next() を実行すると、初回の実行でその時点の DocumentRef が返され、2回目以降はドキュメントの変更があるまで処理をブロックします。DocumentSnapshotIterator.Stop() で変更の監視を終了します。

snapIter := client.Collection("users").Doc("docID").Snapshots(ctx)
defer snapIter.Stop()

for{
  data, err := snapIter.Next()
  if err != nil {
    log.Fatalln(err)
  }
  fmt.Printf("data exist: %+v, data: %+v\n", data.Exists(), data.Data())
}

試しに /users/user1 ドキュメントの snapshot を取得し、Firebaseのコンソールからデータを変更してみます。すぐに変更後の DocumentRef が得られます。

data exist: true, data: map[id:user1 name:username]
data exist: true, data: map[name:newname id:user1]
data exist: true, data: map[name:test id:user1]

変更のイベントが送られてくるのは、ドキュメントが作成・更新・削除された場合です。削除された場合のハンドリングが必要なため、得られたDocumentは存在するかどうかのチェックが必要です。

試しに上記のコードで監視しているドキュメントを削除すると次のような出力が得られます。

data exist: true, data: map[name:test id:user1]
data exist: false, data: map[]

テストに使用したコードはgistに置いています。

Gist: ドキュメントのリアルタイムアップデートを取得する

クエリ結果のリアルタイムアップデートを取得する

クエリ結果のリアルタイムアップデートは、下記のように Query.Snapshots から取得できます。QuerySnapshotIterator.Next() を実行すると、初回の実行でその時点の QuerySnapshot が返され、2回目以降はドキュメントの変更があるまで処理をブロックします。QuerySnapshotIterator.Stop() で変更の監視を終了します。

クエリ結果全てを取得する場合

クエリ結果全てを取得したい場合は QuerySnapshot.Documents からその時点のクエリ結果を取得できます。

試しに /users コレクションで state=1 のユーザを取得するクエリの QuerySnapshotIterator を取得し、Firebaseのコンソールからデータを変更してみます。すぐに変更後の QuerySnapShot が得られます。

initDB()

snapIter := client.Collection("users").Where("state", "==", 1).Snapshots(ctx)
defer snapIter.Stop()

for{
  snap, err := snapIter.Next()
  if err != nil {
    log.Fatalln(err)
  }

  docs, err := snap.Documents.GetAll()
  fmt.Printf("data size: %d\n", snap.Size)
  for i, data := range docs {
    fmt.Printf("data %d, content: %+v\n", i, data.Data())
  }
  fmt.Println()
}
func initDB(){
	colref := client.Collection("users")
	colref.Doc("user1").Set(ctx, map[string]interface{}{
		"id": "user1",
		"name": "username",
		"state": 0,
	})
	colref.Doc("user2").Set(ctx, map[string]interface{}{
		"id": "user2",
		"name": "username",
		"state": 1,
	})
	colref.Doc("user3").Set(ctx, map[string]interface{}{
		"id": "user3",
		"name": "username",
		"state": 3,
	})
	colref.Doc("user4").Set(ctx, map[string]interface{}{
		"id": "user4",
		"name": "username",
		"state": 1,
	})
	colref.Doc("user5").Set(ctx, map[string]interface{}{
		"id": "user5",
		"name": "username",
		"state": 5,
	})
}

上記コードを実行し、user4 の username を変更した後、user4 を削除します。次のように、変更があるたびに、変更後のクエリ結果が得られます。

data size: 2
data 0, content: map[state:1 name:username id:user2]
data 1, content: map[state:1 name:username id:user4]

data size: 2
data 0, content: map[id:user2 state:1 name:username]
data 1, content: map[state:1 name:new_username id:user4]

data size: 1
data 0, content: map[state:1 name:username id:user2]

前回取得した Snapshot からの変更差分のみ取得する場合

前回取得したクエリ結果からの差分のみを取得したい場合は QuerySnapshot.Changes で差分が得られます。Changes の DocumentChange 型は変更の種類 (Add, Remove, Modify)、変更後の DocumentSnapshot、変更前の Index、変更後の Index を保持しています。

QuerySnapshot.Changes は下記のように利用することができます。

snapIter := client.Collection("users").Where("state", "==", 1).Snapshots(ctx)
defer snapIter.Stop()

for{
	snap, err := snapIter.Next()
	if err != nil {
		log.Fatalln(err)
	}

	fmt.Printf("change size: %d\n", len(snap.Changes))
	for _, diff := range snap.Changes {
		fmt.Printf("diff: %+v\n", diff)
	}
	fmt.Println("")
}

試しに前節と同様の操作を行ってみます。変更がある毎にその差分のみを取得できていることが分かります。

change size: 2
diff: {Kind:0 Doc:0xc00014cf00 OldIndex:-1 NewIndex:0}
diff: {Kind:0 Doc:0xc00014d020 OldIndex:-1 NewIndex:1}

change size: 1
diff: {Kind:2 Doc:0xc00014d380 OldIndex:1 NewIndex:1}

change size: 1
diff: {Kind:1 Doc:0xc00014d380 OldIndex:1 NewIndex:-1}

Gist: クエリ結果のリアルタイムアップデートを取得する

変更があるたびに全てのクエリ結果が Firestore から取得されるのか

上述したように、QuerySnapshot.Documents では変更があるたびにクエリ結果全てを取得することができますが、この時 Firestore からクエリ結果を全て再取得しているのでしょうか? もし変更があるたびに再取得しているなら、クエリ結果に変更があるたびに大量のReadが走ってしまい、パフォーマンスや運用コストに悪影響が及びます。

SDK の実装を見たところ、前回取得したクエリ結果を保持し、取得後に起こった変更を順次適用することで、差分のみの取得を行っていました。 該当コードは google-cloud-goのfirestore/query.gogoogle-cloud-goのfirestore/watch.go です。

現在の状態を docTree で持っておいて、computeSnapshotで変更差分を当てています。そのため、通信にかかる時間は特に気にせず、クエリ結果のリアルタイムアップデートを取得できそうです。

まとめ

本記事では、Firestore の Go SDK で Firestore の ドキュメントのリアルタイムアップデートを取得する方法について書きました。リアルタイムアップデートを取得したい対象のSnapshotIterator を取得することでリアルタイムアップデートを取得することができます。対象のドキュメントが決まっている場合は DocumentSnapshotIterator、ある条件を満たすドキュメントや、コレクション内の全てのドキュメントのリアルタイムアップデートが欲しい場合は QuerySnapshotIterator を使うと良いでしょう。

参考

Cloud Firestore でリアルタイム アップデートを入手する https://firebase.google.com/docs/firestore/query-data/listen?hl=ja

Go Doc: cloud.google.com/go/firestore https://godoc.org/cloud.google.com/go/firestore

本記事を書くきっかけとなったツイート https://twitter.com/apstndb/status/1067725995480764416?s=20