开始使用 GraphQL Ruby【基于类的API】

首先
的 Chinese paraphrase for “目的” could be “目标” (mù , which means “objective” or “goal” in English.
本文的目的是总结适用于初学者或即将开始学习GraphQL的人所需要了解的信息,并提供相应索引。
我想要吸引的读者是那些想要开始学习GraphQL或者刚刚开始学习的人。
需要解释的一个背景是,为了增加基于GraphQL的API的数量,同时说明其引入/学习成本并不算高。
另外,我在个人使用的Markdown笔记服务Issus(イシューズ)中整理了使用Graphql所得到的信息。
简要概述
从GraphQL的概念、特点和优势这个角度来讨论,
谈论请求/响应流程、查询、架构、解析、类型、测试、文件处理和错误处理等具体实现细节。
我会简单介绍一下。
如果您有任何需要修正的内容,或者有更好的信息,敬请提出编辑请求。
注意!
-
- graphql-ruby(かつRails)をベースに話すため他Graphqlライブラリでは当てはまらない話もあるかもしれません
-
- 途中図に起こすのを諦めて手書きしたメモがあります
長いぞ!!!
GraphQL有什么用途?
这方面,こんぴゅ总结得很清楚易懂。
GraphQL不是REST的替代品|こんぴゅ|note
只需要一个选择。
-
- APIリクエストを束ねて効率化 = 各所で必要十分なレスポンスを要求できる
-
- スキーマファースト開発
-
- マイクロサービスのAPIゲートウェイ
裏側のデータソースを複数持てたり、他のRESTapiをcallしたり
除此之外,
-
- request parameters / responseに型をつけられる
-
- RESTfulAPIの面倒なエンドポイント設計が不要になる
- クライアントがサーバーの開発を待たなくていい
我认为也有类似的好处。
另外,在视频中,使用全栈教程学习GraphQL基础非常易懂,将概念清晰地总结了起来。
GraphQL的核心在于支持声明式数据获取,客户端可以明确指定需要从API中获取哪些数据。与返回固定数据结构的多个终端不同,GraphQL服务器仅暴露一个终端,并准确地回应客户端所请求的数据。
根据这里所述,我们强烈倾向于在每个人自己定义所需的响应并使用单个API调用来接收相应的响应。背景是由于移动应用的兴起导致API的多样化,以及所需的响应频繁变化,导致每次都需要服务器进行相应的适配,这使得开发速度无法被保持。
GraphQL适用于哪些方面呢?可以参考k0kubun的博客中关于它与RESTful API的冷静比较。
设置!
请实际开发的人跳过此步骤。
这是在Rails项目中引入的前提。
(会涉及到routes等内容,但如果是sinatra等情况,请适当地进行替换)
使用gem安装
首先,我们将引入GraphQL以及可以提供测试客户端界面的graphiql-rails。
gem 'graphql'
group :development do
gem 'graphiql-rails' # graphqlのテスト画面です。後述
end
bundle install
rails generate graphql:install
在routes中添加
接下来,我们将准备用于执行GraphQL的路径和测试客户端的路径。
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/api/v1/graphql"
end
# 適宜好きなpathに設定してください. 今回のケースでは /api/v1/graphqlをエンドポイントにしています
namespace :api, { format: 'json' } do
namespace :v1 do
post "/graphql", to: "graphql#execute"
end
end
现在,完成了
POST /api/v1/graphqlでgraphqlを実行できるように、
GET /graphiql にアクセスすると、grapqhlのテストクライアントにアクセスできるように
变了。
测试客户的情况。

我会继续使用这个查询来通过id获取一个Event。意思是获取Event.find(id),并获取其id和subject。
GraphQL如何工作?
如果你已经使用GraphQL进行了实现,你可以跳过这部分的基本复习。
通信编码

以下为大致流程
-
- 在客户端上构建查询(和变量),然后通过POST请求将其发送到唯一的终点。
在客户端上构建查询和变量是主要的工作,即使是用于数据获取的请求也会使用POST。
服务器端接收到该请求后,解析查询(和变量)并生成响应。
预先定义了用于返回哪个字段以及返回什么样的值的模式作为架构。
实现这个架构是服务器端的主要工作。
是的。
在Rails中,可以根据以下方式查看实际日志和响应。
Started POST "/api/v1/graphql" for ::1 at 2018-07-01 09:22:51 +0900
Processing by Api::V1::GraphqlController#execute as JSON
Parameters:
{
"query"=>"query Event($id:ID = \"\") {\n event {\n id\n subject\n }\n}", # 特殊なGraphql構文をstringで送信
"variables"=>{"id"=>5}, # rocket hashになってる
"operationName"=>"Event", # operationNameはqueryの直後に付けている文字列で、サーバー側でロギングに用いる。省略可能だがあったほうがいい
“graphql"=>{
"query"=>"query Event($id:ID = \"\") {\n event {\n id\n subject\n }\n}",
"variables"=>{"id"=>5},
“operationName"=>"Event"
}
}

现在我们来看看从客户端的角度来看,客户端和服务器的处理过程。
客户编辑
客户端的工作是
-
- 构建查询并向终端发送请求,
-
- 检查响应内容,若存在错误则进行错误处理,
- 基于接收到的响应数据进行绘图或其他操作(此处省略)。
首先,让我们从第一个开始看。
组装查询并向终端发送请求。
发送给GraphQL的参数有两个。一个是查询 (query),另一个是表示要嵌入查询的值的变量 (variables)。
查询是以JSON形式呈现的,但是它有自己独特的语法,需要大致记住它的结构。
根据以下的查询和变量来查看构成要素。
query Event ($id:ID = 1) {
event {
id
subject
}
}
{ "id": 5 }

这样组装的查询和变量将始终以POST方法作为请求发送到唯一的终端点。
这也是 GraphQL 的特点,通过它,客户端可以在每个瞬间通过一次 API 调用获取所需的响应,而无需考虑终端点设计等问题。
根据响应内容进行检查,如果有错误则进行错误处理。
即使Graphql请求存在错误,它也会始终以状态码200进行返回。
(因为Graphql可以同时发送多个查询,因此无法将其中任何一个失败的情况表示为状态码。)
只需一种选择:
作为替代,如果发生错误,需要在响应中包含错误内容(errors),根据其有无进行错误处理。
使用JS的伪代码来追踪处理过程,大致呈现如下图像。
import { API } from 'api' // 何らかのhttpリクエストライブラリ
const response = await API.post('/api/v1/graphql', { query: query, variables: variables })
if (response.errors) {
// do error handling
console.log(response.errors)
return
}
const event = response.data.event
// do something with response
然而,实际上,根据每个客户端的需求,使用相应的GraphQL客户端库较为常见。
客户端库支持构建自定义查询语句和json变量,并根据响应来处理错误。
客户端库有一些主流的选择,例如Facebook开发的Relay(在JavaScript中)和日渐流行的Apollo。官方也推荐了一些适用于JavaScript的客户端库。同时,还有适用于iOS和Android的客户端库可供选择。
我将展示在JavaScript中的伪代码。
$ npm install apollo-boost graphql-tag graphql --save
import ApolloClient = from 'apollo-boost' // 便利なデフォルト設定を加えたapollo client
const client = new ApolloClient({
url: 'api/v1/graphql'
})
import gql from 'graphql-tag'
client.query({
query: gql`
query Event($id:ID!) {
event(id: $id) {
id
subject
}
}
`,
variables: {
id: 1
}
})
.then((response) => {
const event = response.data.event
// do something...
// responseにerrorsがあればthrowしてくれる
}).catch((error) => {
// do something
})
此外,针对js,每个框架都有相应的库,建议将它们结合起来使用。
例如,如果您使用Vue.js,您可以使用https://github.com/Akryum/vue-apollo。
由于客户端的具体实现因客户端而异,因此我将在另一篇文章中提供关于Vue.js的内容。
总结
以下是客户端的工作内容
-
- 根据查询构建请求并发送到终端
构建查询和变量,
发送到单一终端进行POST请求。
通过检查响应内容,进行错误处理
始终返回200。如果有错误,响应中会有errors字段,通过检查是否存在该字段来进行处理。
如果使用客户端库,可以负责构建查询和变量,并进行错误处理。
然后根据接收到的响应数据进行渲染或其他操作。
服务器编程
服务器的工作是
-
- 受け取ったquery, variablesを解釈し
- responseを返す
是的。
回应部分很简单。你几乎可以直接使用通过 “rails generate graphql:install” 自动生成的graphql_controller来处理。
class Api::V1::GraphqlController < Api::V1::BaseController
#
# 省略
#
# POST /api/v1/graphql
def execute
# ensure_hashの実装はこの真下に自動生成されていますが手を入れることは少ないので省略
variables = ensure_hash(params[:variables])
query = params[:query]
# OperationNameはロギングで使う
operation_name = params[:operationName]
# 後述. なくてもいい
context = { current_user: current_user }
# MySchemaは、後述するGraphqlの実体となるclassです。
# executeするとquery, variablesをもとにresponseとなるhashを返します
result = MySchema.execute(
query,
variables: variables,
context: context,
operation_name: operation_name
)
render json: result # hashが返るので、あとは response json: result するだけ。
end
#
# 省略
#
end
Graphql的Schema负责解释接收到的query和variables。
服务器工程师的工作是在Schema中定义”当收到该Field时返回这些数据”。
这个部分从2018/05/18发布的v1.8.0开始有了很大的改变,并且变为了基于类的实现方式。
有关基于类的实现的详细信息可以在官方文档中找到。
虽然如此,由于在网络上可以找到大多数是基于旧有方式的文章,因此本文将采用基于类的写作方式。
终于来到本文的正文部分了。
实施Schema
以下是GraphQL可以处理的三种请求。
GraphQL有三种类型的请求存在。
query
参照系 Read
mutation
更新系 Create Update Delete
subscription
websocketのようにクライアントがサーバーをオブザーブする仕組みを提供します
ちょっとまだ使ったことがないので省略!
> Subscriptions allow GraphQL clients to observe specific events and receive updates from the server when those events occur. This supports live updates, such as websocket pushes. Subscriptions introduce several new concepts:
首先,为了理解GraphQL的基础知识,我们将看一下Query。
查询编辑
GraphQL-Ruby的服务器端整体架构。
首先我们来看一下在graphql-ruby下的处理流程。
-
- GraphqlController#execute
-
- MySchema.execute
-
- 在execute中检视每个Types::QueryType的field
指定field值的类型
描述如何获取与field值相对应的内容,作为resolve处理程序

定义“当这个字段出现时返回这个数据”到Schema中,就是实施第三个的意思。
领域的基本实施方式
首先我们来看一个简单的例子。
这里记录了基本的实现,但其中也包含了无法在基于类的实现中使用的描述。
由于可以传递丰富的选项,查看 gems/graphql-1.8.1/lib/graphql/schema/field.rb 可以获得准确的选项,这样更不容易出错。
“field的实现是” (Field de shì)
-
- 定义字段(名称,类型,描述,是否为空)
-
- 使字段解析
解析=确定字段返回的值的方法
我会按照两个步骤进行操作。
class Types::QueryType < GraphQL::Schema::Object
# 基本は field(name, type, description, null:)
field :ping, String, '疎通確認', null: false
# Types::QueryTypeではfield名と同名のメソッドを定義し(便宜上これをresolveメソッドと呼びます)、その返り値をfieldが返す値として用いる
# ここではpingのfieldにたいして、'pong'という文字列を返すpingメソッドを定義し、
# これをresolveメソッドとしている
def ping
'pong'
end
end
此外,有多种解决方法,我稍后会提及。
在类型(type)中可以定义默认存在的类型(String、Int、Float、Boolean和ID(用于指定整数的字符串或整数类型)),还可以定义与模型相适应的自定义类型。
生成schema
在这个阶段,可以调用GraphQL,但是在此之前,GraphQL会自动从架构中生成API架构。让我们先生成并保留它。
$ rails graphql:schema:dump
Schema IDL dumped into ./schema.graphql
Schema JSON dumped into ./schema.json
在这种状态下,访问测试客户端。

我确认root的QueryType中有一个类型为String的ping字段。并且通过导出schema,它也提供了自动补全和显示文档的功能。
(请暂时忽略event)
使用原始类型的字段实现
在以下示例中,使用event field会返回EventType类型的响应。
同时,我们在这里使用field参数,将传入的id用于检索Event实例,并将其应用于事件字段(resolve)。
class Types::QueryType < GraphQL::Schema::Object
#
# 単体のリソース取得sample
# Types::EventType がオリジナルの型
#
field :event, Types::EventType, null: true do
description 'イベントをidで1件取得' # fieldのdescription引数はブロック内で定義してもOK
#
# 基本 argument(arg_name, arg_type, description, required:)
#
argument :id, ID, 'イベントのID', required: true
end
#
# argumentがあるとき、resolveメソッドは必須キーワード引数としてそれを受け取れる
#
def event(id:)
Event.find(id)
end
end
建议查看gems/graphql-1.8.1/lib/graphql/schema/argument.rb,其中包含了argument的官方说明文档。
定义类型
接下来,我们将实现一个名为EventType的自定义类型。
由于EventType在resolve方法中返回了Event模型的实例,
因此我们希望将Event模型的属性作为字段添加到EventType中。
# eventモデル
class CreateEvents < ActiveRecord::Migration[5.2]
def change
create_table :events do |t|
t.references :user, foreign_key: true
t.string :subject, null: false
t.text :body
t.timestamps
end
end
end
class Types::EventType < GraphQL::Schema::Object
# [1] resolveのinference(推論)と、resolveのバリエーション
field :id, ID, null: false
field :subject, String, null: false
field :body, String, null: false
# [2] resolveメソッド内で利用できるhelperメソッド: object, context
field :bodyHtml, String, null: false
# snake_caseであることに注意してください。
# resolveメソッド名を `camelize(:lowder)`した値がfield名のresolveに用いられます
def body_html
object.decorate.body_html
end
# [3] 型の種類にはいくつかある
field :createdAt, ScalarTypes::DateTime, null: false
end
我们来看三个要点。
[1] 解决的推断和解决的变体
在 field 中并不总是需要一个 resolve 函数,有时候它会自动推断。
这个条件是,
如果我能通过发送领域名称作为消息来处理当前的”object”(也就是说,如果该领域名称与属性、方法的名称相匹配),那么我就可以利用其返回值。
是的。
“今正在处理的对象是指查询所组装的’上一层级的值’。”
query Event {
{
event { # eventからみたobjectはrootなのでnil。なおMySchema.executeにoptionを渡すとrootのobjectを指定できる
id
subject # id, subjectからみたobjectはeventに対してresolveした値のこと。
}
}
}
例如,在当前情况下,可以通过field :event解析Event.find(id)来获得事件。
由于Event模型具有id、subject和body作为属性,所以在EventType的id、subject和body方面不需要特别解决。
如果允许不存在于模型中的字段的情况
-
- bodyHtmlのようにresolveメソッドをその場に書く
-
- fieldのキーワード引数として method: を用いる
field名と異なるメソッドを定義した際にそれを適用させる. あまり使わないかも
objectにたいしてhashに対するkeyのようにアクセスできれば hash_key: を用いる
例えば hash_key: :body すると event.bodyではなく、 event[:body] で値を取得する
resolverクラスを用いる
これを使うのは特定の用途に限定したほうが良いと公式docsにあります
可以做这件事。
基本上,解决方法是在root中编写resolve方法,或者在Object类型中编写与model对应的方法和属性。另外,将object作为decorator实例,并在decorator中编写与field对应的方法,可能可以避免在model中添加用于视图逻辑的代码。
可以在root上指定object,通过传递名为root_value的选项到MySchema.execute来实现。
current_team = current_user.team
result = MySchema.execute(query, { root_value: current_team })
你可以在这里查看execute的其他选项。
[2] 在resolve方法中可使用的helper:对象,上下文
在编写resolve方法时,可以使用名为object和context的辅助方法。
根据之前解释的[1],object指的是组装了查询的“上级值”。对于根节点,如果不指定root_value,则为nil。
上下文(context)是指在GraphqlController#execute中传递给execute的上下文。在这里,我们预期将应用程序范围内使用的信息(如current_user)填充到graphql内部并传递。
例如,我们会执行对获取的对象进行访问权限检查等操作。
def event(id:)
ev = Event.find(id)
raise Forbidden, 'このイベントにはアクセスできません' unless context[:current_user].can?(:read, ev)
ev
rescue Forbidden => e
raise GraphQL::ExecutionError, e.message # 後述
end
[3] 有几种不同的型号。
在这里实现的EventType将成为一种叫做Object的类型。
除了GraphQL,还有其他的选项
Scalar 例えば日付など、primitiveなデータ型を定義する
Enum enumを定義
Input 入力値を型にして再利用
Interfaces 文字通り
Unions 文字通り
Non-Null !の取扱
有几种方便记住和使用的类型和类型语法,明确了会很方便,但需要根据需要进行试验。官方文档在这里。
在这里,我仅举例展示了Scalar的实现。
class ScalarTypes::DateTime < GraphQL::Schema::Scalar
description '日付型'
def self.coerce_input(value, _context)
Time.zone.parse(value)
end
def self.coerce_result(value, _context)
I18n.l(value, format: :to_date)
end
end
根据EventType中的写法,现在可以使用字段 :createdAt,ScalarTypes::DateTime,null: false。
错误处理
在 GraphQL 中,当出现错误时,会在响应的顶级中包含一个名为 “errors” 的键,以返回错误内容。
如果Graphql无法捕获错误,则会返回与其内容相关的错误,其中状态码为400系和500系。
由于graphql客户端不预期以这些状态码返回,因此必须始终执行异常处理。
因此,请用中文进行本地化的重新描述,只需要一个选项:
因此,请用中文进行本地化的重新描述,只需要一种选择:
- 建立一个机制,确保不管出现什么情况,即使Graphql出现无法捕获的错误,也一定要进行救援。
进行以下两个点
[1] 将错误转化为可以捕获的 GraphQL 错误。
我們將再次檢查先前的錯誤處理程序。
def event(id:)
ev = Event.find(id)
raise Forbidden, 'このイベントにはアクセスできません' unless context[:current_user].can?(:read, ev)
ev
rescue Forbidden => e
raise GraphQL::ExecutionError, e.message
end
通过使用Graphql::ExecutionError,在这里可以将发生的错误封装在errors键中,并以状态码200返回。
同时,错误消息可以传递给这个错误,它会被存储在errors的message中。
使用继承了 Graphql::ExecutionError 的错误会更清晰和适当地提供错误信息。
class GraphQL::Forbidden < GraphQL::ExecutionError
def initialize
super('このイベントにはアクセスできません')
end
#
# 以下のようにraiseすることで、errorsキーに{ code: 'SOME_ERROR_CODE' }を追加し、クライアント側でエラー種別を判別しやすくできます
# raise GraphQL::ExecutionError.new(message, { extensions: { code: 'SOME_ERROR_CODE' } })
#
# to_hをoverrideすることで、このextensionsを常に返すようにできます。
#
def to_h
super.merge({ "extensions" => {"code" => Graphql::"FORBIDDEN"} })
end
end
可以用以下方式改写:
def event(id:)
ev = Event.find(id)
raise GraphQL::Forbidden unless context[:current_user].can?(:read, ev)
ev
end
[2] 创建一个机制,即使出现无法捕捉到的GraphQL错误也要确保进行救援。
你可以使用Graphql::ExecutionError或其子类使客户端可以正确地传递错误信息,以在可能发生错误的地方出现错误。然而,在所有可能出错的地方都了解错误的发生是困难的,所以需要一个安全网点。
这个实现可以在MySchema中完成。
class GraphQL::BadRequest < GraphQL::ExecutionError; end
class GraphQL::NotFound < GraphQL::ExecutionError; end
class MySchema < GraphQL::Schema
#
# 起きたエラーを全て拾う
#
rescue_from(Exception) do |error|
case error
when ActiveRecord::RecordInvalid
GraphQL::BadRequest.new 'リクエストが正しくありません'
when ActiveRecord::RecordNotFound
GraphQL::NotFound.new '見つかりません'
else
GraphQL::ExecutionError.new '原因不明のエラーが発生しました'
end
end
query(Types::QueryType)
mutation(Types::MutationType)
end
在这里,关于错误的描述是有的,但是没有写得很详细,所以您需要阅读 gems/graphql-1.8.1/lib/graphql/schema.rb 来了解更多信息。
获取多个资源和分页
当想要提供每页获取 N 个 Event 的分页功能时,使用 GraphQL 可以很容易地实现。实际上,在 GraphQL 中已经预先集成了基于游标的分页实现,几乎只需通过查询就可以实现,而且几乎没有额外的成本。
公式文件中有说明。
大致上的任务是
-
- 在服务器端使用connection,
- 在客户端上根据connection设置查询/处理与connection相匹配的响应。
是的。
服务器端
带有分页功能的字段定义非常简单。
# 型の指定として、EventTypeを複数返す型として、Types::EventType.connection_typeを指定します
# NOTE! class-based api以前はfieldの代わりにconnectionを用いましたがもう使いません。
field :events, Types::EventType.connection_type, null: false do
description 'イベント一覧をpaginationで取得します'
argument :query, String, required: false # イベントを検索する文字列
end
def events(query: nil)
Event.search({
subject_or_body_cont: query
}).result.order(created_at: :desc)
end
只需这一个选项,即可获得基于光标的分页。
客户端方
首先,我們需要構建查詢。
query Events(
$lastEndCursor: String = "",
$offset: Int = 0,
$query: String = ""
) {
# first, afterがgraphqlによって与えられるfield引数です
# first 該当の範囲で頭から何件取得するか
# after 指定したcursor以後を取得対象とする
# (queryは独自に追加したfield引数。あれば、それを元にsubject, bodyを検索します)
events(first: 5, after: $lastEndCursor, query: $query) {
# connectionが提供するページネーション情報
pageInfo {
hasPreviousPage
hasNextPage
endCursor
startCursor
}
# connectionはアイテムのリストをedgesでラップする
edges {
cursor # 現在位置を示す。afterにわたす
node { # 一つ一つのobjectをnodeとして示す
id
subject
body
}
}
}
}
这是返回的 JSON 结果。
{
"data": {
"events": {
"pageInfo": {
"hasNextPage": true,
"hasPreviousPage": false,
"startCursor": "Mw==",
"endCursor": "Nw=="
},
"edges": [
{
"cursor": "Mw==",
"node": {
"id": "12",
"subject": "test"
}
},
{
"cursor": "NA==",
"node": {
"id": "11",
"subject": "asdfasdf"
}
},
{
"cursor": "NQ==",
"node": {
"id": "10",
"subject": "asdfadsf"
}
},
{
"cursor": "Ng==",
"node": {
"id": "9",
"subject": "asdfasdf"
}
},
{
"cursor": "Nw==",
"node": {
"id": "8",
"subject": "ほげ"
}
}
]
}
}
}
根据响应,可以看出键值中夹杂着”edges”和”node”。
在客户端上需要进行从这些键值中提取所需信息的处理,其中会出现客户端上固有的GraphQL代码,如”edges”和”node”。
请判断是否要设立与GraphQL进行交互的层,或者果断接受整个代码库中的GraphQL。
应当如下的响应流程(伪代码)
const response = await API.post('/graphql', query, variables)
// edgesからevent(node)を取り出す必要がある
// この処理がAPIコールする各所に散らばるのはちょっと嫌。
this.store.events = response.data.events.edges.map(item => item.node)
突变
Mutation的实现过程与Query几乎完全相同。
服务器端
class MySchema < GraphQL::Schema
query(Types::QueryType)
mutation(Types::MutationType) # <= ADD
end
# Mutationのrootです. 継承するのはGraphQL::Schema::Objectなので、通常のObjectタイプの型ですね
class Types::MutationType < GraphQL::Schema::Object
# fieldのmutationキーワード引数にmutation処理を渡します
field :updateEvent, mutation: Mutations::UpdateEvent
end
到目前为止的实现与查询几乎没有区别。
实际的变更处理也与查询并没有太大不同,唯一不同的是定义了一个名为”resolve”的方法,在此处编写处理逻辑。
class Mutations::UpdateEvent < GraphQL::Schema::Mutation
null false
argument :id, ID, required: true
argument :subject, String, required: false
argument :body, String, required: false
# 公式のサンプルでは、mutationのresponseは { object, errors } を返していました
# トップレベルのerror
field :event, Types::EventType, null: false
field :errors, [String], null: false
# 引数にargumentが入ってくるのはqueryと同じ挙動
def resolve(id:, subject: nil, body: nil)
event = Event.find(id)
event.subject = subject if subject
event.body = body if body
if event.save
{ event: event, errors: [] }
else
{
event: event,
errors: event.errors.full_messages
}
end
end
# こういう書き方でもいいかも 後述
# def resolve(id:, subject: nil, body: nil)
# event = Event.find(id)
# event.subject = subject if subject
# event.body = body if body
# event.valid? && event.save!
# rescue ActiveRecord::InvalidRecord => e
# # topレベルにエラーの種類、エラーの追加フィールドにバリデーションエラーを含めて返す
# raise GraphQL:ExecutionError.new('invalid parameter', extensions: { additional_messages: event.errors.full_messages })
# end
end
客户端方
在客户端侧,一切保持不变,只需要构建查询和变量然后发送。
需要注意的是,即使在Mutation处理中,参数名也是查询。
如果发生更新或保存失败,需要意识到错误信息可能不会出现在顶层的位置。
mutation UpdateEvent($id:ID!, $subject:String = null, $body: String = null) {
updateEvent(id: $id, subject: $subject, body: $body) {
event {
id
subject
body
}
errors
}
}
变量
{ "id": 5, "subject": "11111111" }
回應 (huí
{
"data": {
"updateEvent": {
"event": {
"id": "5",
"subject": "11111111",
"body": "asfasdf"
},
"errors": [] // エラーがあった場合ここに入ってくる
}
}
}
我正在研究一种方法,使用 save! 而不是 save 来引发错误,并在 errors 的 extensions 中返回 event.errors.full_messages。
悬念
我們將從這裡開始,大致列出GraphQL的問題,並逐一檢視。
-
- テスト
-
- N+1問題
- ファイルアップロード
考试
公式文档中写道,不需要进行类似于E2E测试的测试,而是要进行更轻量级的测试。
测试GraphQL模式行为的最简单方法是将行为抽取到单独的对象中,并在隔离环境下测试这些对象。对于Rails框架来说,你不是通过运行控制器测试来测试模型的对吗?同样地,你可以在不进行端到端测试的情况下,单独测试系统的“低层”部分。
-
- schema内に(たとえばresolveメソッドなど)メソッドをあまり書かずにモデルに寄せること
- (そうしておけば)schemaは殆どの場合modelのmethod, attributeを取得するため、そのテストをちゃんと書くこと
听说是这样的。
如果一定要进行E2E测试,可以测试MySchema.execute,因为graphql_controller很薄。
describe '#execute' do
subject {
MySchema.execute(
query,
{
context: context,
variables: variables
}
)
}
let(:context) { {} }
let(:variables) { {} }
describe 'get event by id' do
context 'when Event.find(id) exists' do
let(:query) {
%|
query Event ($id:ID!) {
event(id: $id) {
id
subject
}
}
|
}
let(:variables) { {id: 1} }
it 'returns event with id, subject' do
#
expect(subject).have_key(:id)
expect(subject).have_key(:subject)
end
end
end
end
我有一种预感,它比传统的请求规范更快(尚未进行测量)。
N+1问题
Graphql中由于查询的自由性,容易出现 N+1 问题。例如,
query Events {
events {
edges {
id
subject
user { # ここ!
id
}
}
}
}
当使用这样的查询时,每个事件都要针对用户进行一次一次的询问。
在这个解决方案中,通过遍历SQL查询并收集相关联的ID(可以使用其他列),然后使用收集到的ID一次性发送批量查询的机制,graphql-batch提供了这个功能(由Shopify制作)。
安装
gem 'graphql-batch'
bundle install
class MySchema < GraphQL::Schema
query(Types::QueryType)
mutation(Types::MutationType)
use(GraphQL::Batch) # 読み込み
end
让我们简单实施一下。
由于`gem`公式中的示例能够正常运行,我们将直接使用这个示例。
请参考以下链接:
https://github.com/Shopify/graphql-batch/blob/master/examples/record_loader.rb
# eventはuser_idをもち、belongs_to :user であるとします
class Types::EventType < GraphQL::Schema::Object
field :id, ID, null: false
field :subject, String, null: false
field :body, String, null: false
field :bodyHtml, String, null: false
def body_html
object.decorate.body_html
end
field :createdAt, ScalarTypes::DateTime, null: false
#
# UserTypeは別途定義したとします。本来eventにuserメッセージを送れるのでなにもせずともresolveできますが、
# それを用いず、graphql-batchを用いたresolveをします
#
field :user, Types::UserType, null: false
#
# resolveではuserインスタンスを返さず、一旦Promiseを返し、sql問い合わせを保留します
# Promiseの詳細は https://github.com/lgierth/promise.rb にあります
#
def user
# Loaders::RecordLoader.new(User, User.primary_key).load(object.user_id)と同じ
Loaders::RecordLoader.for(User).load(object.user_id)
end
end
class Loaders::RecordLoader < GraphQL::Batch::Loader
#
# @param [ActiveRecord::Base] model
# !@param [String] column
#
def initialize(model, column: model.primary_key)
@model = model
@column = column.to_s
@column_type = model.type_for_attribute(@column)
end
#
# loadするたびにあとで検索に用いるkey(通常id)をためこみ、Promiseを返す
#
# @param [Any] key ... 通常はid
# @return [Promise]
#
def load(key)
super @column_type.cast(key)
end
#
# 貯めたkeys(ids)を元に一括でレコードを取得 query(keys)のかしょ
# Promiseとしていた箇所をfulfill = 完了し、実際のレコードを返す
#
def perform(keys)
query(keys).each do |record|
# value: idの値。
value = @column_type.cast(record.public_send(@column))
# 溜め込んだPromiseのうち、key = 1のpromiseに対してrecordを割り当ててPromiseを完了 = fulfillする
# 同じidをkeyにもつPromiseが複数あっても、一括でfulfillされる
fulfill(value, record)
end
# 残っているkeyに対するPromiseはrecordが見つからなかったことを意味するので
keys.each { |key| fulfill(key, nil) unless fulfilled?(key) }
end
private
def query(keys)
scope = @model
scope.where(@column => keys)
end
end
这次的是一种属于Loader类型的Event,其中有用户ID存在。还有另一种属于has_many的Loader也提供了例子,因此可以参考那个来解决N+1的问题。
请提供更具体的上下文,以便我能够准确地为您进行中文的同义转述。
以下是两个博客的链接:
文件处理
编写一个像发送文件那样的Mutation需要一些额外的工作,但实际编写时一切都会顺利进行,没有特别困难的地方。
如何在Apollo + Rails(GraphQL服务器)中创建用于上传文件的mutation方法 – Qiita
只要使用apollo-client,文件上传就可以顺利实现。
最后
了解请求/响应流程、查询、Schema、解析、类型、测试、文件处理、错误处理等几个主要知识点,基本上应该能够完成开发的整个过程!
虽然还有一些未解释的地方,像`execute`的选项,`MySchema`的配置,缓存,监控等等,但我认为这些将会在以后逐步了解。
如果有疑惑,建议长时间浏览 http://graphql-ruby.org/guides。
另外,由于graphql-ruby的实现并不十分复杂(而且官方文档有些地方可能有点过时),建议您也可以去查看一下实现代码。
希望这篇文章能够促进更多人开始使用Graphql, 祈祷这种情况能够发生!
如果有喜欢的人在,请等待他们的小费捐赠!

他可以参考的文章链接
-
- GraphQL Coding Style Guide | bitjourney Kibela
https://bitjourney.kibe.la/shared/entries/1f26acbe-315f-42b8-9ecd-11bcaac5b697
Scrapbox
https://scrapbox.io/graphql-ruby-ja/
GraphQLは何に向いているか – k0kubun’s blog
世のフロントエンドエンジニアにApollo Clientを布教したい – Qiita
https://qiita.com/seya/items/26c8a0dc549a10efcdf8
RailsでGraphQL APIを作る時に悩んだ5つのこと | スペースマーケットブログ
GraphQL | Issus(イシューズ)
https://issus.me/projects/141