[RAILS] How to implement Pagination in GraphQL (for ruby)

background

In Carely of our service, the server side is implemented by Ruby (on Rails), and the exchange of data with the front (Vue) is implemented by graphQL. Pagination in Rails often uses a gem called Kaminari, but in the case of graphQL, Relay-Style Cursor Pagination seems to be standard, so I tried both implementation methods.

About the gem and version you are using (as of June 26, 2020)

kaminari is version 1.2.1 graphql-ruby is version 1.10.10 is.

Implementation using Relay-Style Cursor Pagination

(URL of graphql-ruby Pagination description) https://graphql-ruby.org/pagination/using_connections.html

Server-side implementation example

Describe to use Pagination Plugin in Schema Class as shown below.


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

Use the description :: connection_type to define the Query to which you want to add Pagination functionality.

field :users, Types::UserType::connection_type, null: true do
  .
  .
  argument :name, String, "name", required: false
  .
end

That's all for the server-side implementation.

Front side query call example

You will be able to specify parameters for first (last), after (before). With the query below, 10 items will be acquired from first and 10 items will be acquired from after by specifying after. For the character string specified in after, specify the character string obtained by cursor. Also, a field called pageInfo can be specified, and is there a previous page or a next page? You can get the position of the start and end cursors.


query MyQuery {
  users (first: 10, after: "xxxx") {
    pageInfo {
      hasPreviousPage  
      hasNextPage
      endCursor
      startCursor
    }
   edges {
     cursor
        node {
          firstName
          lastName
          mailAddress
          age
          .
          .
        }
      }
   ##Can be taken with nodes
   nodes {
     firstName
     lastName
     mailAddress
     age
     .
     .
   }


##Example result
{
  "data": {
    "users": {
      "pageInfo": {
        "hasPreviousPage": false,
        "hasNextPage": true,
        "endCursor": "MTA",
        "startCursor": "MQ"
      },
      "edges": [
        {
          "cursor": "MQ",
          "node": {
            "firstName": "Hogehoge",
            "lastName": "Fuga Fuga",
            "mailAddress": "[email protected]",
            "age": "20"
          }
        },
        {
          "cursor": "Mg",
          "node": {
            "firstName": "Hogehoge 2",
            "lastName": "Fuga Fuga 2",
            "mailAddress": "[email protected]",
            "age": "30"
          }
        },
        .
        .
     ],
     "nodes": [
       {
         "firstName": "Hogehoge",
         "lastName": "Fuga Fuga",
         "mailAddress": "[email protected]",
         "age": "20"
       },
       {
         "firstName": "Hogehoge 2",
         "lastName": "Fuga Fuga 2",
         "mailAddress": "[email protected]",
         "age": "30"
       },
        .
        .
     ]

Implementation using Kaminari

The general methods used for Pagination in kaminari are as follows.

#Get the first page divided into 10 cases
User.page(1).per(10)
#total number
User.page(1).per(10).total_count
#number of tolal pages
User.page(1).total_pages
#Number of pages per page
User.page(1).limit_value
#Current number of pages
User.page(1).current_page
#Number of next pages
User.page(1).next_page
#Number of previous pages
User.page(1).prev_page
#Whether it is the first page
User.page(1).first_page?
#Whether it is the last page
User.page(1).last_page?

Implementation example when using kaminari function with GraphQL

Create a Type for Pagination as shown below.


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

Create ʻUserType and ʻUsersType that returns multiple User information and Pagination as follows.


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


Add the following processing to Query to return pagination information.


#Page with argument,Added to be able to pass per
field :users, Types::UserType, null: true do
  .
  .
  argument :name, String, "name", required: false
  argument :page, Int, required: false
  argument :per, Int, required: false
  .
end

#Use kaminari pagination if there are arguments page and per
def users(**args)
  .
  .
  users = User.page(args[:page]).per(args[:per])
  {
     users: users,
     pagination: pagination(users)
  }
end

#Return count using kaminari method
def pagination(result)
  {
    total_count: result.total_count,
    limit_value: result.limit_value,
    total_pages: result.total_pages,
    current_page: result.current_page
  }
end

This is an example of a query that retrieves the first page divided into 10 items and the result.


query MyQuery {
  users (per:10, page:1) {
    pagination {
      currentPage
      limitValue
      totalCount
      totalPages
    }
   users {
     firstName
     lastName
     mailAddress
     age
     .
     .
   }
}
   

##Example result
{
  "data": {
    "users": {
      "pagination": {
        "currentPage": 1,
        "limitValue": 10,
        "totalCount": 100,
        "totalPages": 10
      },
      "users": [
        {
          "firstName": "Hogehoge",
          "lastName": "Fuga Fuga",
          "mailAddress": "[email protected]",
          "age": "20"
        },
        {
          "firstName": "Hogehoge 2",
          "lastName": "Fuga Fuga 2",
          "mailAddress": "[email protected]",
          "age": "30"
        },
        .
        .
     ]

About proper use

Relay-Style Cursor Pagination It looks good because it is easy to use if you just search for information with API. Since it only has the location information by cursor, if you want to create a UI that displays the total number of cases and the total number of pages on the front side, Custom ) Seems to need to create a connection.

kaminari If you use only in-house engineers and create a UI that displays the total number of cases and the total number of pages on the front side, using kaminari is less man-hours.

Personally, I think it would be better to create a custom connection using Relay-Style Cursor Pagination because it suits the style of graphQL.

Recommended Posts

How to implement Pagination in GraphQL (for ruby)
How to iterate infinitely in Ruby
Try to implement Yubaba in Ruby
How to install Bootstrap in Ruby
How to implement search functionality in Rails
How to implement Kalman filter in Java
How to get date data in Ruby
[Ruby] How to use slice for beginners
How to implement ranking functionality in Rails
How to implement asynchronous processing in Outsystems
[For beginners] How to debug in Eclipse
How to create a query using variables in GraphQL [Using Ruby on Rails]
How to create pagination for a "kaminari" array
[Rails] How to implement unit tests for models
[For beginners] How to implement the delete function
How to implement gem "summer note" in wysiwyg editor in Ruby on Rails
How to implement a like feature in Rails
How to implement optimistic locking in REST API
[Ruby on Rails] How to install Bootstrap in Rails
How to build the simplest blockchain in Ruby
How to implement image posting function using Active Storage in Ruby on Rails
How to implement UICollectionView in Swift with code only
[Ruby] How to use standard output in conditional branching
How to implement guest login in 5 minutes in rails portfolio
How to implement a like feature in Ajax in Rails
[Ruby on Rails] How to write enum in Japanese
Rails / Ruby: How to get HTML text for Mail
[Ruby On Rails] How to reset DB in Heroku
How to launch another command in a Ruby program
How to handle TSV files and CSV files in Ruby
Summary of how to implement default arguments in Java
How to resolve SSL_connect error in PayPal Ruby SDK
How to use Ruby return
Ruby: How to use cookies
[Ruby] How to write blocks
[Rails] How to implement scraping
[Java] How to implement multithreading
[Ruby on Rails] How to implement tagging / incremental search function for posts (without gem)
[Ruby] How to split each GraphQL query into a file
How to change a string in an array to a number in Ruby
[For Ruby beginners] Explain how to freely delete array elements!
How to retrieve the hash value in an array in Ruby
How to implement infinite scrolling (page nate) in Swift TableView
How to display a graph in Ruby on Rails (LazyHighChart)
How to implement one-line display of TextView in Android development
How to install Web application for each language in Nginx
[Ruby] How to batch convert strings in an array to numbers
How to use Lombok in Spring
How to specify validation for time_field
How to run JUnit in Eclipse
How to install JMeter for Mac
[Rails] How to write in Japanese
How to run Ant in Gradle
How to master programming in 3 months
How to install ruby through rbenv
How to learn JAVA in 7 days
Run GraphQL Ruby resolver in parallel
How to get parameters in Spark
[Ruby] How to use any? Method
Implement a gRPC client in Ruby
How to introduce jQuery in Rails 6