GraphQL RubyのPaginationについて
GraphQLのページネーションは、Relayを用いたカーソル型のページネーションを用いるのが一般的です。しかしページ番号や、総件数などの情報が必要な場合には、Relayを拡張して対応する必要があります。
GraphQLについて
GraphQLは、「APIのクエリ言語」です。従来のRESTful APIは複数のエンドポイントを持ちますが、GraphQLは単一のエンドポイントしか持ちません。このエンドポイントに、クライアントが逐次必要な情報を問い合わせることで、効率よくデータの取得が可能になります。
GraphQL Rubyの導入
GraphQL本体をGUI用のgraphiql-railsをGemfileに記載し、bundle installします。
# Gemfile
gem 'graphql'
group :development do
gem 'graphiql-rails'
end
その後、初期セットアップ用のコマンドを実行します。/graphql以下にファイルが自動生成されます。
$ bundle exec rails generate graphql:install
またエンドポイントをroutes.rbに記載します。
# routes.rb
# /graphqlがエンドポイント
post "/graphql", to: "graphql#execute"
# GUI用
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/api/v1/graphql"
end
Schema / Queryの実装
今回はArticle Schemaを作成し、author_idでArticle検索を行うQueryを例にあげます。まずArticleのSchemaを作成します。
# app/graphql/types/article_type.rb
class Types::ArticleType < Types::BaseObject
field :id, ID, 'ID', null: false
field :title, String, '記事名', null: false
field :author, Types::AuthorType, '著者', null: false #別途定義
end
# app/graphql/types/author_type.rb
class Types::AutherType < Types::BaseObject
field :id, ID, 'ID', null: false
field :name, String, '名前', null: false
end
query_type.rbにauthor_id検索を行う、エンドポイントを作成します。型定義には複数リソースを取得するために、Relayのconnection_typeを使用します。
# app/graphql/types/query_type.rb
class Types::QueryType < Types::BaseObject
field :search_articles, Types::ArticleType.connection_type, null: false do
description '記事を著者で検索する'
argument :author_id, ID, '著者ID', required: true
end
def search_articles(author_id: nil)
Article.where(author_id: author_id)
end
end
Relayを用いたページネーション
まずはRelayを用いたページネーションです。GUIツール(/graphiql)から以下のクエリーを作成し、実行してみます。connection_typeを指定するとfirst, afterが自動で引数に追加されます。
query {
# first 何件取得するか
# after 指定したcursor以後を取得対象とする
searchArticles(first: 10, after: 'hogehoge', authorId: 1) {
# Relayが提供するページネーション情報
pageInfo {
hasPreviousPage
hasNextPage
endCursor
startCursor
}
# Relayはアイテムのリストをedgesでラップする
edges {
cursor # 現在位置
node {
id
title
}
}
}
}
実行するとこのようなレスポンスになります。
{
"data": {
"searchArticles": {
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "hogehoge",
"endCursor": "piyopiyo"
},
"edges": [
{
"cursor": "hogehoge",
"node": {
"id": "123",
"title": "タイトル"
}
},
{
"cursor": "NA==",
"node": {
"id": "125",
"title": "あああ"
}
},
・・・
{
"cursor": "piyopiyo",
"node": {
"id": "156",
"subject": "たああいとる"
}
}
]
}
}
}
次のリソースを取得するには、first / afterを次のように設定します。
searchArticles(first: 10, after: 'piyopiyo', authorId: 1)
このような形式は、無限スクロールやタイムライン型のアプリには向いていますが、ページネーションで切り替えるタイプのアプリには向いていません。
ページ番号付きのページネーション
Relayを拡張して、総件数やページ番号付きのレスポンスが返却されるようにしていきます。今回は以下のfieldを追加します。totalCount : 総件数, totalPage:総ページ数, nowPage:現在のページ
まずはRelayを拡張し、totalCount, totalPage, nowPage のfieldを定義します。
# app/graphql/types/base_connection.rb
class Types::BaseConnection < GraphQL::Types::Relay::BaseConnection
field :total_count, Int, null: false, description: '総件数'
def total_count
object.nodes.size
end
field :total_page, Int, null: false, description: '総ページ数'
def total_page
(total_count.to_f / limit.to_f).ceil
end
field :now_page, Int, null: false, description: '現在のページ'
def now_page
page
end
protected
def page
object.arguments[:page] || 1
end
def limit
object.arguments[:limit] || 10
end
end
edgesを使用せず、直接articlesを得たいため、BaseConnectionを継承したSearchArticlesConnectionを作成します。そしてこのConnectionを使用するtypeを作成します。
# app/graphql/types/search_articles_connection.rb
class Types::SearchArticlesConnection < Types::BaseConnection
field :articles, [Types::ArticleType], null: false, description: '検索結果'
def articles
object.nodes.page(page).per(limit)
end
end
# app/graphql/types/search_articles_type.rb
class Types::SearchArticlesType < Types::BaseObject
class << self
def connection_type_class
@connection_type_class ||= Types::SearchArticlesConnection
end
end
end
最後にページ用のクエリー、page, limit をQueryTypeに追加し、connection_typeにSearchArticleTypeを指定します。
# app/graphql/types/query_type.rb
class Types::QueryType < Types::BaseObject
field :search_articles, Types::SearchArticlesType.connection_type, null: false do
description '記事を著者で検索する'
argument :author_id, ID, required: true
argument :page, Int, required: false
argument :limit, Int, required: false
end
def search_articles(author_id: nil, page: 1, limit: 10)
Article.where(author_id: author_id)
end
end
このようにすることで、ページ番号付きのページネーションを作成できます。以下にクエリーと実行例を記載します。
# query
query {
searchArticles(page: 1, limit: 10, authorId: 1) {
totalCount
totalPage
nowPage
articles: {
id
title
}
}
}
# result
{
"data": {
"searchArticles": {
"totalCount": 100,
"totalPage": 10
"nowPage": 1
"articles": [
{
"id": "123",
"title": "タイトル"
},
{
"id": "125",
"title": "あああ"
},
・・・
]
}
}
}
まとめ
GraphQL Rubyのページネーションの実装方法を2種類紹介(Relayを使ったページネーション・Relayを拡張したページ番号付きのページネーション)しました。使用用途によって使い分けられるといいですね!
参考
雑に始める GraphQL Ruby【class-based API】