Series: go-firestore

Go で Cloud Firestore のデータをサーバーサイドでページング

ビュー側で「次へ」ボタンを押下するとサーバーサイドで次のページのデータを取得する、というもののやり方がわからなかったが、zenn でやり方を共有してくれていた人がいたので実装してみた。

開発環境

  • go version go1.16.5 linux/amd64

本文

コードは 前回 の続きからで、下記にアップした。

https://gitlab.com/k1350/daybreak_sample/-/tree/firebase_paging_sample

今回やった内容だが、下記記事の内容を Go で書き直した。

APIサーバーでFirestoreを使っているときのカーソルベースのページネーション手法

ドキュメントスナップショットを使ってページングする方法がわからなくて React でのデータ取得を最近やっていたのだが、上記記事を見てドキュメントのパスを渡せばいいとわかったので実装した。

コード全体は GitLab を見ていただくとして、今回やった部分のみかいつまんで表示する。なおエラー処理は適当。

まずビュー側の変更だが、下記の 17 - 19 行目が今回追加した「次へ」ボタンになる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>{{.Title}}</title>
  </head>
  <body>
    <ul>
    {{ range .Content }}
      <li>
      text: <span style="white-space: pre-line;">{{ .text }}</span><br>
      created: {{ .created.Format "2006/01/02 15:04:05 JST" }}<br>
      updated: {{ .updated.Format "2006/01/02 15:04:05 JST" }}
      </li>
    {{ end }}
  </ul>
  {{ if .HasNext }}
  <a href="/?next={{ .NextId }}">Next</a>
  {{ end }}
  </body>
</html>

HasNext = true だったら「次へ」ボタンを表示し、クエリストリングで NextId を渡すという仕様とした。

サーバーサイドの実装は下記のように変更した。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
func rootHandler(w http.ResponseWriter, r *http.Request) {
	next := r.URL.Query().Get("next")

	pc := client.Collection("posts")

	var iter *firestore.DocumentIterator
	if next != "" {
		dsnap, err := pc.Doc(next).Get(ctx)
		if err != nil {
			log.Fatalln(err)
		}
		iter = pc.OrderBy("created", firestore.Desc).StartAt(dsnap.Data()["created"]).Limit(limit + 1).Documents(ctx)
	} else {
		iter = pc.OrderBy("created", firestore.Desc).Limit(limit + 1).Documents(ctx)
	}

	docs, err := iter.GetAll()
	if err != nil {
		log.Fatalln(err)
	}
	hasNext := len(docs) > limit

	lastId := ""
	if hasNext {
		lastDoc := docs[len(docs)-1]
		lastDocRef := lastDoc.Ref
		lastId = lastDocRef.ID
		docs = docs[0 : len(docs)-1]
	}

	var posts []map[string]interface{}
	for _, doc := range docs {
		item := doc.Data()
		v := make(map[string]interface{})
		v["text"] = item["text"]
		v["created"] = item["created"].(time.Time).In(jst)
		v["updated"] = item["updated"].(time.Time).In(jst)
		posts = append(posts, v)
	}

	p := Page{
		Title:   "Test",
		Content: posts,
		HasNext: hasNext,
		NextId:  lastId,
	}
	err = templates["index"].Execute(w, p)
	if err != nil {
		log.Fatalln(err)
	}
}

追加した処理概要

クエリストリングで next の値が渡されたら、それは次のページの先頭データの ID なので、ID 指定でドキュメントを取得する。

その後、そのドキュメントから始まるデータを limit + 1 個分取得する。next が渡されなかったら先頭から limit + 1 個分取得する。

もし取得したデータの個数が limit より多ければ次のページが存在するので、その情報を hasNext 変数に保持しておく。

次のページが存在する場合、取得したデータの一番最後の ID を lastId 変数に格納する。1

そして取得したデータを limit 個数分になるよう切り出す。

あとは前回までと同じようにデータを詰め、ビューに渡すときに hasNext, lastId を渡すようにすればよい。

所感と今後

ドキュメントスナップショットそのものをビュー側と受け渡せないので、ドキュメントスナップショットを使ったクエリカーソルは無理なのかなと思っていたが、パスを取り出してそれを受け渡せばいいというのを見て、そうか……と思って実装した。

どのようなプロパティが取り出せるのかはリファレンスをきちんと見ていかないとわからないのもあり、公式チュートリアルを見ただけでは思いつけなかった。

ただやはり「前へ」「次へ」でページングするのには向いていないように思うので、Go を API サーバとして考え、フロントは React なりなんなりで前回表示したデータに次のデータをつけ足して表示していく方針で考えてみる。

以上


  1. 参考にした記事では Path を取り出すと書いてあるが、同じような書き方を Go でやる場合取り出すのは ID では? と思い ID にした。(リファレンス) ↩︎