概要
前回の記事に続き、Rails × GraphQL でページネーションを実装していきます。
本記事では、GraphQLにおける2つの主要なページネーション方式について解説し、Rails環境での実装方法を紹介します。
ページネーションについて
ページネーションには大きく2つの方式があります。
各方式の詳細と、GraphQLでの実装パターンをみていきましょう🙋♂️
カーソルページネーション
カーソルページネーションは、GraphQLの仕様を策定したRelayが推奨する方式です。
各アイテムに対して一意の「カーソル」を発行し、そのカーソルを基準にして「次の〇件」「前の〇件」という形でデータを取得します。
特徴
- データの追加・削除が発生しても、カーソルを基準にするため結果が一貫している
- 大量データでも効率的にページング可能
- Relay仕様に準拠しており、GraphQLのエコシステムと親和性が高い
ユースケース
- 無限スクロール
- SNSのタイムライン
- 大規模なデータセット
オフセットページネーション
オフセットページネーションは、従来のWebアプリケーションでよく使われる方式です。
「何件目から」「何件取得するか」を指定してデータを取得します。
特徴
- 実装・理解が容易
- 「〇ページ目」という概念があり、従来のUIと相性が良い
- 任意のページに直接ジャンプ可能
ユースケース
- 検索結果
- ページ番号ナビゲーションが必要なケース
実装
ページネーションの方式について理解したところで実装していきましょう🙋
カーソルページネーション
カーソルページネーションの実装は比較的簡単です。
graphql-ruby
では、connection_type
を指定するだけで自動的にRelay
スタイルのページネーションが有効になります。
1. クエリの実装
module Types
class QueryType < Types::BaseObject
# -- 追加 --
field :users, Types::UserType.connection_type, null: false
def users
User.all
end
# -- ここまで --
end
end
この実装により、以下の引数が使えるようになります。
-
first
/last
:⠀⠀取得するアイテム数 -
after
/before
: 指定カーソル以降/以前のアイテムを取得
2. クエリ実行
実装が完了したところでクエリを実行していきましょう!
{
users(first: 5){
edges{
cursor
node{
id
name
}
}
pageInfo{
endCursor
hasNextPage
startCursor
hasPreviousPage
}
}
}
🔍 解説
-
first: 5
により、最初の5件のユーザーデータを取得 - カーソル情報と、各ユーザー(
node
) のID
とname
を取得 - ページ情報(次/前のページがあるか、先頭/末尾のカーソル)を取得
Response
レスポンスデータから以下のことがわかります。
- カーソルは各レコードの
ID
をBase64
エンコードした値 - 次ページを取得するには
after: "Mw"
を指定する
{
"data": {
"users": {
"edges": [
{
"cursor": "MQ",
"node": {
"id": "2",
"name": "matsumoto"
}
},
{
"cursor": "Mg",
"node": {
"id": "3",
"name": "honda"
}
},
{
"cursor": "Mw",
"node": {
"id": "4",
"name": "sato"
}
}
],
"pageInfo": {
"endCursor": "Mw",
"hasNextPage": true,
"startCursor": "MQ",
"hasPreviousPage": false
}
}
}
}
オフセットページネーション
オフセットページネーションはgraphql-ruby
で標準サポートされていませんが、カスタムタイプを作成することで実装できます。
ページネーションの Gem
である kaminari
と組み合わせた例を紹介します。
1. ページネーション型の作成
まず、ページネーション関連の情報を格納する型を定義します。
module Types
class PaginationType < Types::BaseObject
field :total_count, Integer, null: true
field :limit_value, Integer, null: true
field :total_pages, Integer, null: true
field :current_page, Integer, null: true
end
end
この型には以下の情報が含まれます。
-
total_count
: 全レコード数 -
limit_value
: 1ページあたりの表示件数 -
total_pages
: 全ページ数 -
current_page
: 現在のページ番号
2. コンテナ型の作成
次に、ページネーション情報と実際のデータを格納するコンテナ型を作成します。
module Types
class UsersType < Types::BaseObject
field :pagination, PaginationType, null: true
field :posts, [PostType], null: true
end
end
- データ本体(
posts
)とメタ情報(pagination
)を分離
3. クエリの実装
最後に、query_type.rb
にオフセットページネーションを使用するフィールドを追加します。
# frozen_string_literal: true
module Types
class QueryType < Types::BaseObject
# --- 追加 ---
field :users, Types::UsersType, null: false do
argument :page, Integer, required: false, default_value: 1
argument :per_page, Integer, required: false, default_value: 15
end
def users(page:, per_page:)
users = User.all.page(page).per(per_page)
{
users:,
pagination: pagination(users)
}
end
private
def pagination(result)
{
total_count: result.total_count,
limit_value: result.limit_value,
total_pages: result.total_pages,
current_page: result.current_page
}
end
# --- ここまで ---
end
end
- 引数に
page
とper_page
を定義(デフォルト値も設定) - Kaminariの
.page()
と.per()
メソッドを使用 - レスポンスは
users
とpagination
の複合構造
4. クエリ実行
{
users(page: 1, perPage: 3) {
users {
id
name
}
pagination {
totalCount
currentPage
totalPages
limitValue
}
}
}
- 1ページ目のデータを取得(3件ずつ)
- ユーザーの
ID
、name
を取得
まとめ
睡魔に襲われながら頑張って書きました🙆♂️
寝ぼけているかもしれないので、Typo や 不足等あればコメントください...!