Rails × GraphQL でAPI開発｜ページネーションを実装する

# 概要

前回の記事に続き、Rails × GraphQL でページネーションを実装していきます。

https://qiita.com/PenPe/items/482ee4dc02f44fdc3983

本記事では、GraphQLにおける2つの主要なページネーション方式について解説し、Rails環境での実装方法を紹介します。

# ページネーションについて
ページネーションには大きく2つの方式があります。
各方式の詳細と、GraphQLでの実装パターンをみていきましょう🙋‍♂️

### カーソルページネーション

カーソルページネーションは、GraphQLの仕様を策定したRelayが推奨する方式です。
各アイテムに対して一意の「カーソル」を発行し、そのカーソルを基準にして「次の〇件」「前の〇件」という形でデータを取得します。

![ChatGPT Image 2025年4月23日 22_32_01 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842674/fea8c624-4f05-4f44-bc80-3de834628542.png)

#### 特徴
- データの追加・削除が発生しても、カーソルを基準にするため結果が一貫している
- 大量データでも効率的にページング可能
- Relay仕様に準拠しており、GraphQLのエコシステムと親和性が高い

#### ユースケース
- 無限スクロール
- SNSのタイムライン
- 大規模なデータセット

### オフセットページネーション

オフセットページネーションは、従来のWebアプリケーションでよく使われる方式です。
「何件目から」「何件取得するか」を指定してデータを取得します。

![ChatGPT Image 2025年4月23日 22_20_10 (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2842674/b80ca276-64bb-4e76-9f58-6e269db879aa.png)

#### 特徴
- 実装・理解が容易
- 「〇ページ目」という概念があり、従来のUIと相性が良い
- 任意のページに直接ジャンプ可能

#### ユースケース
- 検索結果
- ページ番号ナビゲーションが必要なケース

# 実装

ページネーションの方式について理解したところで実装していきましょう🙋

## カーソルページネーション

カーソルページネーションの実装は比較的簡単です。
`graphql-ruby`では、`connection_type`を指定するだけで自動的に`Relay`スタイルのページネーションが有効になります。

### 1. クエリの実装

```ruby
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. クエリ実行

実装が完了したところでクエリを実行していきましょう！

```graphql
{
  users(first: 5){
    edges{
      cursor
      node{
        id
        name
      }
    }
    pageInfo{
      endCursor
      hasNextPage
      startCursor
      hasPreviousPage
    }
  }
}
```

#### 🔍 解説
- `first: 5` により、最初の5件のユーザーデータを取得
- カーソル情報と、各ユーザー（`node`） の `ID` と `name` を取得
- ページ情報（次/前のページがあるか、先頭/末尾のカーソル）を取得

### Response

レスポンスデータから以下のことがわかります。
- カーソルは各レコードの `ID` を `Base64` エンコードした値
- 次ページを取得するには `after: "Mw"` を指定する

```json
{
  "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. ページネーション型の作成

まず、ページネーション関連の情報を格納する型を定義します。

```ruby
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. コンテナ型の作成

次に、ページネーション情報と実際のデータを格納するコンテナ型を作成します。

```ruby
module Types
  class UsersType < Types::BaseObject
    field :pagination, PaginationType, null: true
    field :posts, [PostType], null: true
  end
end
```

- データ本体（`posts`）とメタ情報（`pagination`）を分離

### 3. クエリの実装

最後に、`query_type.rb`にオフセットページネーションを使用するフィールドを追加します。

```ruby
# 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. クエリ実行

```graphql
{
  users(page: 1, perPage: 3) {
    users {
      id
      name
    }
    pagination {
      totalCount
      currentPage
      totalPages
      limitValue
    }
  }
}
```

- 1ページ目のデータを取得（3件ずつ）
- ユーザーの `ID`、`name` を取得

# まとめ

睡魔に襲われながら頑張って書きました🙆‍♂️
寝ぼけているかもしれないので、Typo や 不足等あればコメントください...！