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

4 minute read

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": "hogehoge@example.com",
            "age": "20"
          }
        },
        {
          "cursor": "Mg",
          "node": {
            "firstName": "Hogehoge 2",
            "lastName": "Fuga Fuga 2",
            "mailAddress": "hogehoge2@example.com",
            "age": "30"
          }
        },
        .
        .
     ],
     "nodes": [
       {
         "firstName": "Hogehoge",
         "lastName": "Fuga Fuga",
         "mailAddress": "hogehoge@example.com",
         "age": "20"
       },
       {
         "firstName": "Hogehoge 2",
         "lastName": "Fuga Fuga 2",
         "mailAddress": "hogehoge2@example.com",
         "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": "hogehoge@example.com",
          "age": "20"
        },
        {
          "firstName": "Hogehoge 2",
          "lastName": "Fuga Fuga 2",
          "mailAddress": "hogehoge2@example.com",
          "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.