はじめに
GraphQL Ruby
では connection_type
を使ってカーソルベースのページネーションを容易に実装することができます。また、デフォルトではカーソルベースとなっていますが、少しカスタマイズするだけでオフセットベースのページネーションへの変更も可能です。既にたくさんの記事や知見が公開されているので、それらを参考に自分でも組んでみました。
カーソルベースのページネーション
「ページ表示時には最初のプロジェクト5件が表示されている → ページ下部までスクロールすると次のプロジェクト5件が読み込まれる」といったUIを想定します。
プロジェクト一覧を取得するresolver
にて、レスポンスを connection_type
として定義します。
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
とはいえ、下記のようなオフセットベースのページネーションでの実装が必要な局面もあるでしょう。
その場合も、cursor
をちょっと変更するだけでページネーションの実現が可能です。下記の記事をもとに、オフセットベースのページネーションに変更してみます。
まずは、app/lib/custom_encoder
ディレクトリに新たに offset_encoder.rb
を用意します。cursor
のエンコードをこれでカスタマイズするわけですが、そのままの数字で良いので引数の txt
をそのまま返すようにしています。
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
で、先程定義したエンコーダーを使用するように設定します。
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でページネーション(オフセットベースとカーソルベース)