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.
ruby2.7.1 rails6.0.3 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.
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
column | Mold |
---|---|
name | string |
string |
user.rb
class User < ApplicationRecord
has_many :posts, dependent: :destroy
end
column | Mold |
---|---|
title | string |
description | string |
post.rb
class Post < ApplicationRecord
belongs_to :user
end
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)
GraphQL has a Type (defined for each model), and according to that Type, query is executed and data is acquired.
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 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]")
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.
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",
}
]
}
}
Next, try to get the Post associated with User. Create ObjectType and Query for User in the same way.
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)
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
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.
If you need the data of post.user
, you can get it with the following query.
query{
posts{
id
title
description
user{
id
}
}
}
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.
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
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]]
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]]
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
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
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