memoirの1週間の振り返り機能でGraphQL + Firestoreでクエリカーソルを使用したページングを実装しました。
Pull Request
↑だと、ページング処理はうまく行かないパターンがあったので、さらに以下で修正
実装
GraphQLのページングのフィールド定義は公式でドキュメント化されているので、こちらに沿って実装
今回は、インフィニティスクロール形式で戻る事は想定しなくてOKなので以下の用に定義しました。
type PageInfo {
endCursor: String!
hasNextPage: Boolean!
}
type Item {
"アイテムID"
id: ID!
"ユーザーID"
userID: String!
"タイトル"
title: String!
"カテゴリーID"
categoryID: Int!
"日付"
date: Time!
like: Boolean!
dislike: Boolean!
"作成日時"
createdAt: Time!
"更新日時"
updatedAt: Time!
}
type ItemsInPeriodEdge {
node: Item
cursor: String!
}
type ItemsInPeriod {
pageInfo: PageInfo!
edges: [ItemsInPeriodEdge!]!
}
input InputItemsInPeriod {
after: String
first: Int!
startDate: Time!
endDate: Time!
}
type Query {
"期間でアイテムを取得する"
itemsInPeriod(input: InputItemsInPeriod!): ItemsInPeriod!
}
設計的には、variablesのfirstで何件取得か決めて、afterにカーソルの位置を設定することでページングの途中からデータを取得開始します。
input InputItemsInPeriod {
after: String
first: Int!
startDate: Time!
endDate: Time!
}
また、ResponseのendCursorで次の開始カーソルを返して、hasNextPageで次のページが存在するかを返すことでfrontend側でページングのアクセスを終了させる判定を行います。
type PageInfo {
endCursor: String!
hasNextPage: Boolean!
}
本当はGraphQLにtotalCountを実装したかったのですが、FirestoreにはRDBのカウント的な処理が存在しないので今回はスルーしました。
もしカウントさせたい場合は、以下の記事のようにCloud Functionを作成してカウント数自体を保持させるみたいですが、管理の手間がかかるのでやめました。
www.sukerou.com
これでGraphQLの設計はOKです。
ここから、Firestore側のクエリカーソルの処理を実装していきます。 クエリカーソルについては以下を参照
クエリカーソルの実装は以下になります
// GetItemUserMultipleInPeriod 期間でアイテムを取得する
func (re *ItemRepository) GetItemUserMultipleInPeriod(ctx context.Context, f *firestore.Client, userID []string, startDate time.Time, endDate time.Time, first int, cursor ItemsInPeriodCursor) ([]*model.Item, error) {
var items []*model.Item
query := f.CollectionGroup("items").Where("UserID", "in", userID).Where("Date", ">=", startDate).Where("Date", "<=", endDate).OrderBy("Date", firestore.Asc).OrderBy("CreatedAt", firestore.Asc)
if cursor.ID != "" {
ds, err := getItemCollection(f, cursor.UserID).Doc(cursor.ID).Get(ctx)
if err != nil {
return nil, err
}
query = query.StartAfter(ds)
}
matchItem := query.Limit(first).Documents(ctx)
docs, err := matchItem.GetAll()
if err != nil {
return nil, err
}
for _, doc := range docs {
var item *model.Item
doc.DataTo(&item)
items = append(items, item)
}
return items, nil
}
カーソルが処理がある場合は、カーソルデータから対象のドキュメントのスナップショットを取得して、StartAfterに設定すればOKです。 (ドキュメントでは)
if cursor.ID != "" {
ds, err := getItemCollection(f, cursor.UserID).Doc(cursor.ID).Get(ctx)
if err != nil {
return nil, err
}
query = query.StartAfter(ds)
}
これで以下のようにデータのページングの実装が完了しました。