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

## 背景
弊社サービスのCarelyではサーバサイドはRuby(onRails)で実装されておりフロント(Vue)とのデータのやりとりをgraphQLで実装しています。
RailsでのPaginationはKaminariというgemをよく使うのですが、graphQLの場合はRelay-Style Cursor Paginationがstandardっぽいので両方の実装方法を試してみました。

## 使っているgem, versionについて(2020/6/26現在)
[kaminari](https://github.com/kaminari/kaminari)はversion 1.2.1
[graphql-ruby](https://github.com/rmosolgo/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を使うための記述をします。

```ruby

class MySchema < GraphQL::Schema
  .
  .
  use GraphQL::Pagination::Connections
  .
end
```

Paginationの機能を追加したいQueryの定義に`::connection_type`という記述を使用します。

```ruby
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カーソルの位置を取得することができます。

```graphql

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で使う一般的なメソッドは以下になります。

```ruby
# 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を作成します。

```ruby

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` を作成します。

```ruby

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に以下のような処理を追加します。

```ruby

# 引数で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とその結果の例です。


```graphql

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を作るのであれば[カスタム](https://graphql-ruby.org/pagination/custom_connections.html)で`connection`を作成する必要がありそうです。

**`kaminari`**
社内のエンジニアしか使わず、フロント側でトータル件数、トータルページ数を表示するUIを作るのであればkaminariを使った方が工数的にはかからないです。

個人的には`Relay-Style Cursor Pagination`を使ってカスタムconnectionを作っていく方がgraphQLのスタイルに合っているので良いのではないかと思います。