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