見出し画像

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

Relay

雑に始める GraphQL Ruby【class-based API】


この記事が気に入ったらサポートをしてみませんか?