背景
弊社サービスのCarelyではサーバサイドはRuby(onRails)で実装されておりフロント(Vue)とのデータのやりとりをgraphQLで実装しています。
RailsでのPaginationはKaminariというgemをよく使うのですが、graphQLの場合はRelay-Style Cursor Paginationがstandardっぽいので両方の実装方法を試してみました。
使っているgem, versionについて(2020/6/26現在)
kaminariはversion 1.2.1
graphql-rubyはversion 1.10.10
です。
Relay-Style Cursor Paginationを使っての実装
(graphql-rubyのPaginationの説明のURLです)
https://graphql-ruby.org/pagination/using_connections.html
サーバサイド実装例
下記のようにSchema ClassにPaginationのPluginを使うための記述をします。
class MySchema < GraphQL::Schema
  .
  .
  use GraphQL::Pagination::Connections
  .
end
Paginationの機能を追加したいQueryの定義に::connection_typeという記述を使用します。
field :users, Types::UserType::connection_type, null: true do
  .
  .
  argument :name, String, "名前", required: false
  .
end
サーバサイドの実装は以上です。
フロントサイド queryの呼び出し例
first(last), after(before) のparameterを指定できるようになります。
下記のqueryだとafterの指定場所からfirst(先頭)10件取得になります。
afterに指定する文字列はcursorで取得された文字列を指定します。
また pageInfoというfieldも指定できるようになり、前ページ、次ページがあるか?やstart、endカーソルの位置を取得することができます。
query MyQuery {
  users (first: 10, after: "xxxx") {
    pageInfo {
      hasPreviousPage  
      hasNextPage
      endCursor
      startCursor
    }
   edges {
     cursor
        node {
          firstName
          lastName
          mailAddress
          age
          .
          .
        }
      }
   ## nodesでも取れる
   nodes {
     firstName
     lastName
     mailAddress
     age
     .
     .
   }
## 結果の例
{
  "data": {
    "users": {
      "pageInfo": {
        "hasPreviousPage": false,
        "hasNextPage": true,
        "endCursor": "MTA",
        "startCursor": "MQ"
      },
      "edges": [
        {
          "cursor": "MQ",
          "node": {
            "firstName": "ホゲホゲ",
            "lastName": "フガフガ",
            "mailAddress": "hogehoge@example.com",
            "age": "20"
          }
        },
        {
          "cursor": "Mg",
          "node": {
            "firstName": "ホゲホゲ2",
            "lastName": "フガフガ2",
            "mailAddress": "hogehoge2@example.com",
            "age": "30"
          }
        },
        .
        .
     ],
     "nodes": [
       {
         "firstName": "ホゲホゲ",
         "lastName": "フガフガ",
         "mailAddress": "hogehoge@example.com",
         "age": "20"
       },
       {
         "firstName": "ホゲホゲ2",
         "lastName": "フガフガ2",
         "mailAddress": "hogehoge2@example.com",
         "age": "30"
       },
        .
        .
     ]
Kaminariを使っての実装
kaminariでのPaginationで使う一般的なメソッドは以下になります。
# 10件ごとに分割した1ページ目を取得する
User.page(1).per(10)
# tolal件数
User.page(1).per(10).total_count
# tolalページ数
User.page(1).total_pages
# 1ページの件数
User.page(1).limit_value
# 現在のページ数
User.page(1).current_page
# 次ページ数
User.page(1).next_page
# 前ページ数
User.page(1).prev_page
# 最初のページかどうか
User.page(1).first_page?
# 最後のページかどうか
User.page(1).last_page?
GraphQLでkaminariの機能を使う場合の実装例
以下のようなPagination用のTypeを作成します。
module Types
  class PaginationType < Types::BaseObject
    field :total_count, Int, null: true
    field :limit_value, Int, null: true
    field :total_pages, Int, null: true
    field :current_page, Int, null: true
  end
end
以下のように UserType と複数のUser情報とPaginationを返す UsersType を作成します。
module Types
  class UserType < Types::BaseObject
    field :uuid, String, null: true
    field :first_name, String, null: true
    field :last_name, String, null: true
    field :mail_address, String, null: true
    field :age, String, null: true
    .
    .
  end
end
module Types
  class UsersType < Types::BaseObject
    field :pagination, PaginationType, null: true
    field :users, [UserType], null: true
  end
end
pagination情報を返すためQueryに以下のような処理を追加します。
# 引数でpage, perを渡せるように追加
field :users, Types::UserType, null: true do
  .
  .
  argument :name, String, "名前", required: false
  argument :page, Int, required: false
  argument :per, Int, required: false
  .
end
# 引数 page、perがあればkaminariのpaginationを使用
def users(**args)
  .
  .
  users = User.page(args[:page]).per(args[:per])
  {
     users: users,
     pagination: pagination(users)
  }
end
# kaminariのメソッドを使って件数を返す
def pagination(result)
  {
    total_count: result.total_count,
    limit_value: result.limit_value,
    total_pages: result.total_pages,
    current_page: result.current_page
  }
end
10件ごとに分割した1ページ目を取得するqueryとその結果の例です。
query MyQuery {
  users (per:10, page:1) {
    pagination {
      currentPage
      limitValue
      totalCount
      totalPages
    }
   users {
     firstName
     lastName
     mailAddress
     age
     .
     .
   }
}
   
## 結果の例
{
  "data": {
    "users": {
      "pagination": {
        "currentPage": 1,
        "limitValue": 10,
        "totalCount": 100,
        "totalPages": 10
      },
      "users": [
        {
          "firstName": "ホゲホゲ",
          "lastName": "フガフガ",
          "mailAddress": "hogehoge@example.com",
          "age": "20"
        },
        {
          "firstName": "ホゲホゲ2",
          "lastName": "フガフガ2",
          "mailAddress": "hogehoge2@example.com",
          "age": "30"
        },
        .
        .
     ]
使い分けについて
Relay-Style Cursor Pagination
APIで情報を検索するだけなら簡単に使えるので良さそう。
ただcursorによる位置情報を持っているだけなのでフロント側でトータル件数、トータルページ数を表示したりするUIを作るのであればカスタムでconnectionを作成する必要がありそうです。
kaminari
社内のエンジニアしか使わず、フロント側でトータル件数、トータルページ数を表示するUIを作るのであればkaminariを使った方が工数的にはかからないです。
個人的にはRelay-Style Cursor Paginationを使ってカスタムconnectionを作っていく方がgraphQLのスタイルに合っているので良いのではないかと思います。
