【Rails6】利用GraphQL开发API(查询篇)
首先
我使用Rails 6进行了GraphQL开发,并总结了其使用和引入方法。这次我将介绍查询/关联和N+1问题。
开发环境
ruby2.7.1
rails6.0.3
GraphQL -> Ruby2.7.1、Rails6.0.3、GraphQL
GraphQL是什么?
GraphQL是一种用于API请求的查询语言,具有以下特点。
-
- エンドポイントは/graphql1つのみ
-
- Query: データの取得(Get)
- Mutation: データの作成、更新、削除(Create, Update, Delete)
与REST不同的一个重要区别是GraphQL只有一个端点。在REST中,存在多个端点,如/sign_up、/users、/users/1等,而在GraphQL中,只有一个端点,即/graphql。
在REST中,如果需要多个资源,就需要发起多个API请求,而GraphQL只有一个端点,可以一次获取所需的数据,从而使代码更简洁。
2. 这次使用的桌子
這將在兩個資料表中實現,父親資料表是User,子資料表是Post。我們將設定User可以發佈多個Post的一對多關係。
$ rails g model User name:string email:string
$ rails g model Post title:string description:string user:references
$ rails db:migrate
用户表
class User < ApplicationRecord
has_many :posts, dependent: :destroy
end
帖子数据表
class Post < ApplicationRecord
belongs_to :user
end
在Rails中引入GraphQL
安装gem。
gem 'graphql' #追加
group :development, :test do
gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
gem 'graphiql-rails' #開発環境に追加
end
require "sprockets/railtie" #コメントアウトを外す
$ bundle install
$ rails generate graphql:install #GraphQLに関するファイルが作成されます
请在routes.rb中添加以下内容:
在开发环境下,端点为/graphiql,在生产环境下,端点为/graphql。
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' #ここはrails generate graphql:installで自動生成される
end
请参考下面的详细总结,了解在Rails 6的API模式下如何使用GraphQL(包括错误处理方法)。
查询获取用户列表的命令
在GraphQL中,我们可以定义类型(针对每个模型),然后按照这些类型执行查询来获取数据。
创建用户的ObjectType
使用下面的命令来创建用户类型。
添加感叹号(!)将添加 null:false。
$ rails g graphql:object User id:ID! name:String! email:String!
会生成下述文件。
module Types
class UserType < Types::BaseObject
field :id, ID, null: false # `!`をつけると`null:false`が追加されます。
field :name, String, null: false
field :email, String, null: false
end
end
创建用户的查询
根据刚刚创建的用户类型来生成查询。
module Types
class QueryType < Types::BaseObject
field :users, [Types::UserType], null: false # userを配列で定義する
def users
User.all # user一覧を取得
end
end
end
使用控制台创建用户数据。
$ rails c
$ > User.create(name: "user1", email: "user-1@test.com")
$ > User.create(name: "user2", email: "user-2@test.com")
执行查询
准备工作已经完成,现在可以启动服务器并在Graphiql上进行确认。(http://localhost:3000/graphiql)
$ rails s
执行以下查询。
query{
users{
id
name
email
}
}
然后会返回JSON格式的响应。
由于users的Type是数组,所以它是一个数组。
{
"data": {
"users": [
{
"id": "1",
"name": "user1",
"email": "user-1@test.com"
},
{
"id": "2",
"name": "user2",
"email": "user-2@test.com"
}
]
}
}

如果只请求并接收必要的数据
如果不需要所有列的数据,则可以修改查询。
例如,您也可以仅获取用户的ID。
query{
users{
id
}
}
“回应”
{
"data": {
"users": [
{
"id": "1",
},
{
"id": "2",
}
]
}
}
5. 协会 (xié huì)
接下来,我们尝试获取与用户相关联的帖子。
我们将以同样的方式创建用户的ObjectType和Query。
生成Post的ObjectType。
$ rails g graphql:object Post id:ID! title:String! description:String!
将生成以下文件。
为了获取用户数据,我们将添加字段:user,类型为Types::UserType,不允许为空。
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 # この一文を追加。belongs_to :userのようなもの
end
end
在UserType中添加字段: posts, [Types::PostType], null: false。
由于这是多个数据关联,因此使用数组进行定义。
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 # この一文を追加。has_many :postsのようなもの
end
end
$ rails c
$ > Post.create(title: "title", description: "description", user_id: 1)
生成Post的查询
module Types
class QueryType < Types::BaseObject
field :users, [Types::UserType], null: false
def users
User.all
end
# 下記を追加
field :posts, [Types::PostType], null: false
def posts
Post.all
end
end
end
执行查询。
这是一个要执行的查询。
将帖子嵌套在用户中并进行请求。
query{
users{
id
name
email
post{
id
title
description
}
}
}
执行后将返回嵌套的帖子数据。
在REST环境中,可以获取用户的帖子数据。

如果需要post.user的数据,可以使用如下的查询来获取。
query{
posts{
id
title
description
user{
id
}
}
}

引入能够检测到N+1的子弹
由于GraphQL查询是树状结构,所以当存在关联时,容易出现N+1问题。因此,我们建议引入用于检测N+1问题的Bullet gem。
安装Bullet
group :development do
gem 'bullet'
end
$ bundle install
在`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
我试着确认N+1。
当Bullet的安装完成后,让我们来确认一下是否出现了N+1。
query{
users{
id
name
email
posts{
id
title
description
}
}
}
如果执行与之前相同的查询,将会出现以下日志。
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
在您的查询中添加.includes([:posts])的原因是出现了N+1问题。
SQL查询也执行了三次。
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]]
解决N+1的方法
要消除N+1,一般来说,只需要使用includes就可以了。
也可以通过dataloader解决,但这次我们将使用includes。
我们将更改获取用户列表的部分如下:
def users
# User.all # 変更前
User.includes(:posts).all # 変更後
end
让我们通过日志来确认N+1是否已经解决。
警告已经消失了。
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)
由于SQL文减少到两个,N+1问题已经成功解决。
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. 提取resolver
常见的查询
如果按照常规方法编写代码,不管模型如何,fields和方法会被添加到query_type中,使得query_type越来越庞大。
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
使用Resolver来查询
在GitHub的问题中,通过使用Resolver,介绍了一种避免query_type.rb扩大化的最佳实践。
https://github.com/rmosolgo/graphql-ruby/issues/1825#issuecomment-441306410
请只定义field在query_type中。
module Types
class QueryType < BaseObject
field :users, resolver: Resolvers::QueryTypes::UsersResolver
field :posts, resolver: Resolvers::QueryTypes::PostsResolver
end
end
然后,将方法部分按照每个ObjectType切分到Resolver中(我们创建了一个新的 resolvers 目录)。
请务必不要忘记写上 GraphQL::Schema::Resolver,否则会报错,请注意不要忘记。
module Resolvers::QueryTypes
class UsersResolver < GraphQL::Schema::Resolver
type [Types::UserType], null: false
def resolve
User.includes(:posts).all
end
end
end
module Resolvers::QueryTypes
class PostsResolver < GraphQL::Schema::Resolver
type [Types::PostType], null: false
def resolve
Post.all
end
end
end
最后
关于GraphQL,当我在复习时,总结起来比想象中更长。虽然我想写关于rspec和mutation的内容,但我打算留到下次。