6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【GraphQL Ruby】ページネーション実装

Last updated at Posted at 2025-03-26

はじめに

GraphQL Ruby では connection_type を使ってカーソルベースのページネーションを容易に実装することができます。また、デフォルトではカーソルベースとなっていますが、少しカスタマイズするだけでオフセットベースのページネーションへの変更も可能です。既にたくさんの記事や知見が公開されているので、それらを参考に自分でも組んでみました。

カーソルベースのページネーション

「ページ表示時には最初のプロジェクト5件が表示されている → ページ下部までスクロールすると次のプロジェクト5件が読み込まれる」といったUIを想定します。
プロジェクト一覧を取得するresolver にて、レスポンスを connection_type として定義します。

projects_resolver.rb
module Resolvers
  class ProjectsResolver < BaseResolver
    description 'プロジェクト一覧'
    # レスポンスを connection_type で設定
-   type [Types::ProjectType], null: true
+   type Types::ProjectType.connection_type, null: true
    

    def resolve
      Project.all
    end
  end
end

first で取得件数を指定、レスポンスに cursor を追加します。

query Projects {
  projects(first: 5) {
    edges {
      node {
        id
        name
      }
      cursor
    }
  }
}

最初の5件を取得した上で、各 edge にカーソルが付与されています。5件目の「プロジェクト5」のカーソルは NQ ですね。

{
  "data": {
    "projects": {
      "edges": [
        {
          "node": {
            "id": "1",
            "name": "プロジェクト1"
          },
          "cursor": "MQ"
        },
        {
          "node": {
            "id": "2",
            "name": "プロジェクト2"
          },
          "cursor": "Mg"
        },
        {
          "node": {
            "id": "3",
            "name": "プロジェクト3"
          },
          "cursor": "Mw"
        },
        {
          "node": {
            "id": "4",
            "name": "プロジェクト4"
          },
          "cursor": "NA"
        },
        {
          "node": {
            "id": "5",
            "name": "プロジェクト5"
          },
          "cursor": "NQ"
        }
      ]
    }
  }
}

先程のGraphQLクエリの条件に after: "NQ" を追加します。

query Projects {
  projects(first: 5, after: "NQ") {
    edges {
      node {
        id
        name
      }
      cursor
    }
  }
}

すると、プロジェクト6〜プロジェクト10の5件を新たに取得することができました。
すなわち、カーソルベースのページネーションは 「最後の要素のカーソルを取得」→ 「次ページの要素問い合わせ時にカーソルを指定」の繰り返し で実現が可能です。

{
  "data": {
    "projects": {
      "edges": [
        {
          "node": {
            "id": "6",
            "name": "プロジェクト6"
          },
          "cursor": "Ng"
        },
        {
          "node": {
            "id": "7",
            "name": "プロジェクト7"
          },
          "cursor": "Nw"
        },
        {
          "node": {
            "id": "8",
            "name": "プロジェクト8"
          },
          "cursor": "OA"
        },
        {
          "node": {
            "id": "9",
            "name": "プロジェクト9"
          },
          "cursor": "OQ"
        },
        {
          "node": {
            "id": "10",
            "name": "プロジェクト10"
          },
          "cursor": "MTA"
        }
      ]
    }
  }
}

オフセットベースのページネーション

先程のConnection Concepts のページに下記の記述があるように、オフセットベースよりもカーソルベースを GraphQL Ruby ではおすすめしているようです。

Connections have some advantages over offset-based pagination:

  • First-class support for relationship metadata
  • Cursor implementations can support efficient, stable pagination

とはいえ、下記のようなオフセットベースのページネーションでの実装が必要な局面もあるでしょう。

スクリーンショット 2025-03-25 3.36.39.png

その場合も、cursor をちょっと変更するだけでページネーションの実現が可能です。下記の記事をもとに、オフセットベースのページネーションに変更してみます。

まずは、app/lib/custom_encoder ディレクトリに新たに offset_encoder.rb を用意します。cursor のエンコードをこれでカスタマイズするわけですが、そのままの数字で良いので引数の txt をそのまま返すようにしています。

offset_encoder.rb
module CustomEncoder
  module OffsetEncoder
    def self.encode(txt, nonce: false)
      txt
    end
  
    def self.decode(txt, nonce: false)
      txt
    end
  end
end

そして、api_schema.rb で、先程定義したエンコーダーを使用するように設定します。

offset_api_schema.rb
class OffsetApiSchema < GraphQL::Schema
  mutation(Types::MutationType)
  query(Types::QueryType)

  # For batch-loading (see https://graphql-ruby.org/dataloader/overview.html)
  use GraphQL::Dataloader

+ # offsetベースのページネーション
+ cursor_encoder(CustomEncoder::OffsetEncoder)
end

先程と同じくプロジェクトを5件、cursor が"10"よりも後のもの(3ページ目に表示されるもの)を指定してみます。なお、cursor は数値ではなく文字列です。

query Projects {
  projects(first: 5, after: "10") {
    edges {
      node {
        id
        name
      }
      cursor
    }
  }
}

目論見通り、プロジェクト11〜プロジェクト15を取得できました。
「(取得したいページNo - 1) * 1ページあたり表示件数」afterに指定してあげることで、オフセットベースでのページネーションを実現することができます。

{
  "data": {
    "projects": {
      "edges": [
        {
          "node": {
            "id": "11",
            "name": "プロジェクト11"
          },
          "cursor": "11"
        },
        {
          "node": {
            "id": "12",
            "name": "プロジェクト12"
          },
          "cursor": "12"
        },
        {
          "node": {
            "id": "13",
            "name": "プロジェクト13"
          },
          "cursor": "13"
        },
        {
          "node": {
            "id": "14",
            "name": "プロジェクト14"
          },
          "cursor": "14"
        },
        {
          "node": {
            "id": "15",
            "name": "プロジェクト15"
          },
          "cursor": "15"
        }
      ]
    }
  }
}

まとめ

  • connection_type を使用することでページネーションを手早く実装することができる
  • cursor をカスタマイズすることで、オフセットベースのページネーションとして利用することもできる

参考

GraphQL - Connection Concepts
GraphQL - Cursors
graphql-rubyでページネーション(オフセットベースとカーソルベース)

6
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?