18
12

More than 3 years have passed since last update.

GraphQLでのPaginationの実装方法について(for ruby)

Last updated at Posted at 2020-07-01

背景

弊社サービスの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のスタイルに合っているので良いのではないかと思います。

18
12
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
18
12