[Rails 6] API development using GraphQL (Query)

Introduction

Since I developed using GraphQL with rails6, I tried to summarize the introduction method and usage. This time, I will introduce the Query / Association / N + 1 problem.

Development environment

ruby2.7.1 rails6.0.3 GraphQL

1. What is GraphQL?

GraphQL is an API request, so it is a query language and has the following features.

--Only one endpoint / graphql --Query: Get data --Mutation: Create, Update, Delete data

The big difference from REST is that the first endpoint is one. For REST, there are multiple endpoints such as / sign_up, / users, / users / 1, For GraphQL, the only endpoint is / graphql.

REST requires multiple API requests if required by multiple resources, GraphQL has one endpoint, so you can get the data you need in one go </ font> and the code is simple.

2. The table used this time

It is implemented by the parent User table and the child Post table. Let's assume a one-to-many relationship where a user can post multiple posts.

Terminal


$ rails g model User name:string email:string
$ rails g model Post title:string description:string user:references
$ rails db:migrate

User table

column Mold
name string
email string

user.rb


class User < ApplicationRecord
  has_many :posts, dependent: :destroy
end

Post table

column Mold
title string
description string

post.rb


class Post < ApplicationRecord
  belongs_to :user
end

3. Introduce GraphQL to rails

Install gem.

Gemgile


gem 'graphql'    #add to

group :development, :test do
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'graphiql-rails'   #Added to development environment
end

config/application.rb


require "sprockets/railtie"    #Uncomment

Terminal


$ bundle install
$ rails generate graphql:install   #A file related to GraphQL will be created

Add the following to routes.rb The endpoint will be / graphiql in the development environment and / graphql in the production environment.

routes.rb


Rails.application.routes.draw do
  if Rails.env.development?
    # add the url of your end-point to graphql_path.
    mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql" 
  end
  post '/graphql', to: 'graphql#execute'  #Here is rails generate graphql:Automatically generated by install
end

Please refer to the details below. How to use GraphQL in API mode of Rails 6 (including error countermeasures)

4. Query to get User list

GraphQL has a Type (defined for each model), and according to that Type, query is executed and data is acquired.

Create ObjectType for User

Create a User Type with the following command. If you add !, Null: false will be added.

Terminal


$ rails g graphql:object User id:ID! name:String! email:String!

The following files will be generated.

user_type.rb


module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false  # `!`With`null:false`Will be added.
    field :name, String, null: false
    field :email, String, null: false
  end
end

Create User Query

Create a query based on the ʻuser_type` created earlier.

query_type.rb


module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: false #Define user as an array
    def users
      User.all #Get user list
    end
  end
end

Create User data in the console.

$ rails c
$ > User.create(name: "user1", email: "[email protected]")
$ > User.create(name: "user2", email: "[email protected]")

Execute Query

Now that you're ready, start the server and check with Graphiql. (http: // localhost: 3000 / graphiql)

$ rails s

Execute the following query.

query{
  users{
    id
    name
    email
  }
}

Then a json format response will be returned. Since the Type of users is an array, it is an array.

{
  "data": {
    "users": [
      {
        "id": "1",
        "name": "user1",
        "email": "[email protected]"
      },
      {
        "id": "2",
        "name": "user2",
        "email": "[email protected]"
      }
    ]
  }
}

This is the result of connecting to http: // localhost: 3000 / graphiql and executing it. スクリーンショット 2020-09-27 20.32.34.png

When requesting and receiving only the necessary data

Modify the query if you don't need all the column data. For example, you can get only the id of user.

query{
  users{
    id
  }
}

response

{
  "data": {
    "users": [
      {
        "id": "1",
      },
      {
        "id": "2",
      }
    ]
  }
}

5. Association

Next, try to get the Post associated with User. Create ObjectType and Query for User in the same way.

Generate ObjectType for Post

Terminal


$ rails g graphql:object Post id:ID! title:String! description:String!

The following files will be generated. Add field: user, Types :: UserType, null: false to get User's data.

post_type.rb


module Types
  class PostType < Types::BaseObject
    field :id, ID, null: false
    field :title, String, null: false
    field :description, String, null: false
    field :user, Types::UserType, null: false #Add this sentence. belongs_to :Something like user
  end
end

Add field: posts, [Types :: PostType], null: false to UserType. Since multiple data are linked here, define it as an array.

user_type.rb


module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :email, String, null: false
    field :posts, [Types::PostType], null: false #Add this sentence. has_many :Something like posts
  end
end

Terminal


$ rails c
$ > Post.create(title: "title", description: "description", user_id: 1)

Generate Post Query

query_type.rb


module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: false
    def users
      User.all
    end

    #Add the following
    field :posts, [Types::PostType], null: false
    def posts
      Post.all
    end
  end
end

Execute Query

The query to execute. Nest the post to users and request it.

query{
  users{
    id
    name
    email
    post{
      id
      title
      description
    }
  }
}

When executed, the nested Post data will be returned. You can get the data of ʻuser.posts` in the case of REST.

スクリーンショット 2020-09-27 21.00.34.png

If you need the data of post.user, you can get it with the following query.

query{
  posts{
    id
    title
    description
    user{
      id
    }
  }
}

スクリーンショット 2020-09-28 9.54.16.png

6. Introduce Bullet to detect N + 1

GraphQL queries are tree-structured, so associations are prone to N + 1 problems. Therefore, we recommend that you install Bullet, which is a gem that detects N + 1.

Install Bullet

Gemfile


group :development do
  gem 'bullet'
end

Terminal


$ bundle install

Add the following settings to config / environments / development.rb

config/environments/development.rb


config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
  Bullet.rails_logger = true
end

Check N + 1

After installing Bullet, let's check if N + 1 is occurring

query{
  users{
    id
    name
    email
    posts{
      id
      title
      description
    }
  }
}

If you execute the same query as before, the following log will be displayed.

Terminal


Processing by GraphqlController#execute as */*
  Parameters: {"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil, "graphql"=>{"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil}}
  User Load (0.2ms)  SELECT "users".* FROM "users"
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 1]]
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 2]]
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
Completed 200 OK in 36ms (Views: 0.3ms | ActiveRecord: 1.7ms | Allocations: 18427)


POST /graphql
USE eager loading detected
  User => [:posts]
  Add to your query: .includes([:posts])
Call stack

ʻAdd to your query: .includes ([: posts])` says N + 1 is occurring. SQL has also been issued three times.

Terminal


User Load (0.2ms)  SELECT "users".* FROM "users"
Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 1]]
Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 2]]

How to eliminate N + 1

To eliminate N + 1, just ʻincludes as usual. You can solve it with a dataloader, but this time we will use ʻincludes. Change the part that is getting the User list as follows.

query_type.rb


def users
  # User.all  #Change before
  User.includes(:posts).all #After change
end

Now, let's check the log to see if N + 1 has been resolved. The warning is gone.

Terminal


Processing by GraphqlController#execute as */*
  Parameters: {"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil, "graphql"=>{"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil}}
  User Load (0.7ms)  SELECT "users".* FROM "users"
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
  Post Load (1.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (?, ?)  [["user_id", 1], ["user_id", 2]]
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
Completed 200 OK in 57ms (Views: 0.2ms | ActiveRecord: 1.9ms | Allocations: 15965)

Since the number of SQL statements has been reduced to two, N + 1 has been successfully resolved.

Terminal


User Load (0.7ms)  SELECT "users".* FROM "users"
Post Load (1.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (?, ?)  [["user_id", 1], ["user_id", 2]]

7. Extraction of resolver

Normal Query

As you write code normally, fields and methods are added to query_type regardless of the model, so query_type grows bigger and bigger.

query_type.rb


module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: false
    def users
      User.includes(:posts).all
    end

    field :posts, [Types::PostType], null: false
    def posts
      Post.all
    end
  end
end

Query using Resolver

A GitHub issue introduces best practices to avoid bloating query_type.rb by using Resolver. https://github.com/rmosolgo/graphql-ruby/issues/1825#issuecomment-441306410

Only field is defined in query_type.

query_type.rb


module Types
  class QueryType < BaseObject
    field :users, resolver: Resolvers::QueryTypes::UsersResolver
    field :posts, resolver: Resolvers::QueryTypes::PostsResolver
  end
end

Then, the method part is cut out to Resolver for each ObjectType. (A new resolvers directory has been created.) If you forget to write GraphQL :: Schema :: Resolver, an error will occur, so be careful not to forget it.

resolvers/query_types/users_resolver.rb


module Resolvers::QueryTypes
  class UsersResolver < GraphQL::Schema::Resolver
    type [Types::UserType], null: false
    def resolve
      User.includes(:posts).all
    end
  end
end

resolvers/query_types/posts_resolver.rb


module Resolvers::QueryTypes
  class PostsResolver < GraphQL::Schema::Resolver
    type [Types::PostType], null: false
    def resolve
      Post.all
    end
  end
end

At the end

When I summarized GraphQL for my own review, it became unexpectedly long. I also wanted to write about rspec and mutation, but I'd like to do it next time.

Recommended Posts

[Rails 6] API development using GraphQL (Query)
API creation with Rails + GraphQL
MOD development notes using Minecraft 14.4 Fabric API # 1
Try using the Rails API (zip code)
Rails API server environment construction using docker-compose
How to create a query using variables in GraphQL [Using Ruby on Rails]
Rails Development Transition 2020
How to build API with GraphQL and Rails
Using PAY.JP API with Rails ~ Implementation Preparation ~ (payjp.js v2)
[Rails] Development with MySQL
Using PAY.JP API with Rails ~ Card Registration ~ (payjp.js v2)
DSL development using ANTLR 4.7.1
Try using the query attribute of Ruby on Rails
[Rails API + Vue] Upload and display images using Active Storage
[Rails] Create an echo bot using the LINE Messaging API.
Display API definition in Swagger UI using Docker + Rails6 + apipie
Rails6 development environment construction [Mac]
Search function using [rails] ransack
Try using view_component with rails
SNS authentication using Rails google
GraphQL Ruby and actual development
[Rails] Save images using carrierwave
Japaneseize using i18n with Rails
[Rails] Japanese localization using rails-i18n
[Rails] Test code using Rspec
Organize Rails routing using draw
Ajax bookmark function using Rails
Error when using rails capybara
[Rails] Try using Faraday middleware
Detailed tips when using Rails
[Rails 6] Star-shaped review using Raty.js