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) }
これで以下のようにデータのページングの実装が完了しました。