GraphQL + Firestoreでクエリカーソルを使用したページングを実装① - wheatandcatの開発ブログ

wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

GraphQL + Firestoreでクエリカーソルを使用したページングを実装①

memoirの1週間の振り返り機能でGraphQL + Firestoreでクエリカーソルを使用したページングを実装しました。

Pull Request

github.com

↑だと、ページング処理はうまく行かないパターンがあったので、さらに以下で修正

github.com

実装

GraphQLのページングのフィールド定義は公式でドキュメント化されているので、こちらに沿って実装

graphql.org

今回は、インフィニティスクロール形式で戻る事は想定しなくてOKなので以下の用に定義しました。

graph/schema.graphqls

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側のクエリカーソルの処理を実装していきます。 クエリカーソルについては以下を参照

firebase.google.com

クエリカーソルの実装は以下になります

repository/item.go

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

これで以下のようにデータのページングの実装が完了しました。

youtu.be