GraphQL Ruby 的基本使用教程
请注意
目前的graphql-ruby(版本:1.8.x)主要是基于Ruby的类定义,不推荐使用本文中提到的DSL。路线图中提到,将在graphql-ruby 2.0中弃用使用DSL进行.define-style的方式。请参阅官方文档以了解新的基于类的写法。对于日文文章,@gfx先生的”GraphQL”入门与深入——从与REST的比较到API和前端的实现的学习非常全面和易懂。
首先
在我所参与的产品中,我们选择了GraphQL作为前端API。实际使用下来感觉相当不错,但一开始在实施时非常困惑。
因此,我打算专注于服务器端,介绍一下GraphQL的实现方法。
顺便说一下,虽然称之为基础篇,但只是一些基础内容而已。不确定是否还会有进阶篇(可能会写)。
GraphQL很棒。
什么是GraphQL?
根据公式定义,它是一种可用于自己的API的查询语言。
另外还有其他选择。
-
- https://qiita.com/icoxfog417/items/92214aed64f47dfeade5
- http://blog.bitjourney.com/entry/2017/08/19/161308
阅读这些内容,我认为你可以大致了解它是什么样的。
在我个人实施中的感觉是,GraphQL在严格限制REST的基础上解决了API和端点的紧密耦合。在GraphQL中,获取和更新数据基本上使用POST进行(规范中还规定了GET)。
通过将查询添加到POST请求的参数中,可以指定要对API执行的操作。
在这种情况下,客户端可以自由选择任意组合,仅指定所需的数据或想要更新的数据,然后调用API。因此,即使前端的结构或页面导航还没有确定,后端仍然可以继续开发API。
GraphQL的优点在于客户端能够自由选择希望处理的数据,从而减轻了后端实现的负担。
GraphQL的实现
GraphQL仅仅是一个规范,实际的实现方式有很多种。下面举例一种实现方式。
(在awesome-graphql上有GraphQL实现的列表。)
-
- Graphql Ruby
-
- GraphQL.js
-
- Graphcool
- AWS AppSync(Public Preview)
这些可以大致分为两种类型。Graphql Ruby和GraphQL.js是与现有的服务器端框架结合使用的,而Graphcool和AppSync则可以独立完成,并且相对容易实现API服务器。Graphcool最近已经成为开源项目,非常火热。
这次我们将使用与Rails结合使用的GraphQL Ruby来进行讲解。
尝试编写GraphQL。
为了更容易理解(或者更容易解释)GraphQL是什么,最好的方法就是实际上写一些代码来演示。接下来,我们将在创建样例应用的过程中进行讲解。
样例应用在这里 -> https://github.com/sukechannnn/graphql-ruby-demo/
我在plain-rails分支上保留了没有实现GraphQL的状态。我们将使用它来一起实现GraphQL API!(请事先运行bin/setup。)
安装
首先安装GraphQLGem。
gem 'graphql', '1.7.7'
$ bundle install
你好世界!
我们有一个生成器,可以使用它来创建graphql-ruby的模板。
$ rails g graphql:install
然后,将会输出这样的目录结构。其中的内容将在稍后进行解释。
app/graphql
├── graphql_ruby_demo_schema.rb
├── mutations
└── types
├── mutation_type.rb
└── query_type.rb
另外,在浏览器上有一个名为GraphiQL的工具,可以运行和确认GraphQL API的操作。让我们在开发组中安装它的Rails版本。
gem 'graphiql-rails'
$ bundle install
在路由中添加graphiql的路径
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql'
end
好的,请在此之前运行一次`rails s`命令,并尝试访问 http://localhost:3000/graphiql。
如果成功显示graphiql页面,则表示配置已完成。现在我们可以试着发送一个查询。
{
testField
}
如果收到“Hello World!”的回复,那么设置就完成了!
查询、变更和订阅
在GraphQL中,查询(Query)、变更(Mutation)和订阅(Subscription)是进行查询的最基本概念。
询问
这是一种获取数据的机制。
突变
这是一个用于更新数据的机制。
订阅 yuè)
这是用来处理WebSocket的机制。在这里暂不讨论。
查询
接下来,我们将开始实际定义查询并获取数据。首先,让我们确保能够获取由seed_fu插入的数据(db/fixtures/sample_data.rb)。
用户类型
首先是从用户模型开始。因为graphql-ruby有一个生成器,所以我们会使用它。
$ rails g graphql:object User id:ID! name:String! email:String!
然后,将生成以下文件并保存在app/graphql/types/user_type.rb中(为了更易读,稍作格式调整)。
Types::UserType = GraphQL::ObjectType.define do
name 'User'
field :id, !types.ID
field :name, !types.String
field :email, !types.String
end
这里简单解释一下。我们在这里使用GraphQL::ObjectType来定义Types::UserType。ObjectType是基本的对象类型,以后经常会看到它。
name在模式的定义中使用。稍后会详细解释。
field定义了要访问哪些数据。在graphql-ruby中,如果你用与对应模型的属性相同的名称定义field,那么仅仅通过这个就可以获取数据。
可以通过这样来定义UserType,但如果要获取数据,必须将其与Query关联起来。(换句话说,定义的类型可以被重用。)
为了将其与Query关联起来,首先在生成的Types::QueryType内部编写field。
Types::QueryType = GraphQL::ObjectType.define do
name 'Query'
field :user, !Types::UserType do
resolve ->(_obj, _args, ctx) {
ctx[:current_user]
}
end
end
在这里,出现了一个名为resolve的东西。这是用于在GraphQL中编写逻辑的机制。在graphql-ruby中,如果只简单地编写field,那么当该属性(或方法)不存在时,会导致错误。此外,即使属性存在,有时也希望对数据进行处理后返回。使用resolve可以解决这种情况。
在传递给`resolve`的lambda中,有三个参数:`obj`、`args`和`ctx`。
obj は自身のオブジェクトで、例えばUserTypeであればuserの情報が入っています
QueryTypeでは使いません
args はfieldに渡す引数です
ここでは触れません、詳しくはこちら
ctx はログインユーザーの情報など重要な情報を渡すために使います
所以,ctx非常重要。在带有登录功能的应用程序中,传递current_user是惯用做法。这个ctx是由graphql-ruby生成的GraphqlController中指定的。检查一下你会发现在评论中有相关的说明。由于本次示例没有登录功能,我们将使用User.last作为current_user。
class GraphqlController < ApplicationController
def execute
variables = ensure_hash(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
current_user: User.last, # ここでcurrent_userを指定する
}
result = GraphqlRubyDemoSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
end
...
是的,一旦到这里,您就可以使用GraphQL来获取值。请尝试在GraphiQL中发送以下查询。应该会返回您通过seed注册的数据。
{
user {
id
name
email
}
}
在graphql-ruby中,我们使用类似这样的DSL来定义API。起初可能会感到不自然,但一旦熟悉了,就会发现这个格式是相对固定的,所以很容易。 (最近有一个可以在类中定义的PR将被合并并在版本1.8中发布,所以如果您对DSL感到不适,请稍等片刻。)
地址类型
接下来,我们将确保可以访问与用户一对一关联的地址。首先,让我们创建一个地址类型。
$ rails g graphql:object Address id:ID! postal_code:Int! address:String!
如果使用ActiveRecord获取地址,我认为不是直接获取地址,而是从用户中跟踪。 在GraphQL中也是一样。所以,我们需要给用户添加一个字段。
Types::UserType = GraphQL::ObjectType.define do
name 'User'
field :id, !types.ID
field :name, !types.String
field :email, !types.String
field :address, !Types::AddressType # 追加する
end
现在您可以获取地址了。请尝试从GraphiQL发送以下查询。
{
user {
id
name
email
address {
postal_code
address
}
}
}
您是否得到了居住地址和邮政编码的回复?这样一来,基本查询的部分说明基本已经完成了。最后,我将简要介绍一下连接的相关内容。
连结
考虑到用户可以发布推文,模型中的“用户(User)”和“推文(Post)”之间是一对多的关系。为了能够获取这个关系,我们首先需要生成“推文类型(PostType)”。
$ rails g graphql:object Post id:ID! subject:String!
如果对于UserType存在多个PostType的情况,我们可以使用连接(connection)而不是字段来表示它们之间的关联。
Types::UserType = GraphQL::ObjectType.define do
name 'User'
field :id, !types.ID
field :name, !types.String
field :email, !types.String
field :address, !Types::AddressType
connection :posts, !Types::PostType.connection_type # 追加する
end
您可以使用此方式批量获取帖子。请使用GraphiQL提交以下查询。
{
user {
id
name
email
posts {
edges {
node {
id
subject
}
}
}
}
}
我认为您可以获取到”Post”的列表。如果您想要改变顺序,可以使用Resolver并进行”order_by”操作,效果会很好。请查看这里了解更多详细信息。
使用Connection可以非常方便地实现页面分页功能。关于Connection的客户端详细使用方法,请参考以下链接:
https://facebook.github.io/relay/graphql/connections.htm
一些追加说明
由于之前的解释仓促而忽略了一些部分,所以在此简要补充说明。
形状
GraphQL有类型。
Int: A signed 32‐bit integer
Float: A signed double-precision floating-point value
String: A UTF‐8 character sequence
Boolean: true or false
ID: ユニークな文字列で、CacheやConnectionなどRelay由来の仕組みに使われます
此外,您还可以自定义定义ScalarType。
中文中的“Relay”是“中继”的意思。
Relay是使用了Facebook开发的GraphQL的Flux实现。据说可以最大限地发挥React和GraphQL的性能,并带有相当强大的GC等功能。由于是Flux,所以无法与Redux一起使用。如果想要结合Redux&React和GraphQL,可以考虑使用Apollo。
Relay已经以其独特方式对GraphQL进行了扩展,但其中一些功能已被纳入GraphQL。特别是ConnectionType(分页机制),可说是已经成为GraphQL本身的规范。
型号鑲嵌!標記
字段 :name, !types.String 的感叹号表示不可为空。如果没有感叹号,那么该字段是可为空的。通常建议以非空定义。
突变
从这里开始,我们将定义Mutation以便更新数据。接下来要解释的是Relay来源的Mutation。这样写更简洁,清晰易懂,并且可以实现代码的重用。原始的Mutation如果你能理解这里的内容,就可以简单地编写,所以我们省略了解释。
变异:更新地址变异
让我们首先尝试更改地址,生成突变(Mutation)文件。
$ rails g graphql:mutation UpdateAddressMutation
然后,有写着要定义input_field、return_field和resolve的评论。如果定义了这些,看起来就可以创建Mutation了。
输入字段保持不变,该输入将用于变更请求。本次我们将设置要更改的地址和邮政编码。
return_field是返回值。为了在更新后返回每个field的值,我们使用了在Query中定义的Types::AddressType。
然后,通过在resolve中编写更新逻辑完成实施。
Mutations::UpdateAddressMutation = GraphQL::Relay::Mutation.define do
name 'UpdateAddress'
input_field :postal_code, !types.Int
input_field :address, !types.String
return_field :address, !Types::AddressType
resolve ->(_obj, inputs, ctx) {
begin
address = ctx[:current_user].address
address.postal_code = inputs.postal_code
address.address = inputs.address
address.save
rescue => e
return GraphQL::ExecutionError.new(e.message)
end
{ address: address }
}
end
{ address: address }的返回字段可以用哈希定义。至于错误处理中抛出的异常,最好使用GraphQL::ExecutionError。
现在让我们实际进行更新吧。请在GraphiQL中输入以下查询试一试。如果返回的值是更新后的内容,则表示正常运作。
mutation {
updateAddressMutation(input: {
postal_code: 1638001
address: "東京都新宿区西新宿2丁目8−1"
}) {
address {
id
postal_code
address
}
}
}
创建帖子突变(Mutations::CreatePostMutation)
让我们尝试将先前定义的查询(Query)的”PostType”进行投稿。生成一个变更(Mutation)的文件。
$ rails g graphql:mutation CreatePostMutation
我会像先前那样逐步填写内容。这一次是创建操作,所以我会构建内容并将其保存到Post中。
Mutations::CreatePostMutation = GraphQL::Relay::Mutation.define do
name 'CreatePost'
input_field :subject, !types.String
return_field :post, !Types::PostType
resolve ->(_obj, inputs, ctx) {
begin
post = ctx[:current_user].posts.build
post.subject = inputs.subject
post.save
rescue => e
return GraphQL::ExecutionError.new(e.message)
end
{ post: post }
}
end
如果可以的话,让我们尝试从GraphiQL发布!
mutation {
createPostMutation(input: {subject: "良い感じ!"}) {
post {
subject
}
}
}
请尝试查询当前用户的帖子,并且确认帖子已经被添加。
{
user {
id
name
email
posts {
edges {
node {
id
subject
}
}
}
}
}
突变:更新帖子变异
我们来更新一下这次的投稿吧!生成一个UpdatePostMutation。
$ rails g graphql:mutation UpdatePostMutation
由于需要指定要更新的帖子,因此在input_field中添加id。除此之外,其他的工作基本上和之前一样。
Mutations::UpdatePostMutation = GraphQL::Relay::Mutation.define do
name 'UpdatePost'
input_field :id, !types.ID
input_field :subject, !types.String
return_field :post, !Types::PostType
resolve ->(_obj, inputs, ctx) {
begin
post = ctx[:current_user].posts.find(inputs.id)
post.subject = inputs.subject
post.save
rescue => e
return GraphQL::ExecutionError.new(e.message)
end
{ post: post }
}
end
在Types::MutationType中添加一个字段后,尝试进行更新!
mutation {
updatePostMutation(input: {id: "{先程投稿したpostのid}", subject: "めっちゃ良い感じ!!!"}) {
post {
subject
}
}
}
输入类型
Mutation的上面是因为input_field的项目很少,所以很好,但是如果它变得更多,将所有内容都写在Mutation中会变得很麻烦。随着后续解释的description的增加,代码量会迅速增加。此外,在针对同一模型的Create或Update中,input_field经常相同,如果能够重用将会很方便。为此,我们需要使用InputType机制。让我们试着定义UpdateAddressMutation的InputType。在新建的文件app/graphql/types/address_input_type.rb中实现以下内容。
Types::AddressInputType = GraphQL::InputObjectType.define do
name 'AddressInput'
argument :postal_code, !types.Int
argument :address, !types.String
end
要这样做,我们将更改UpdateAddressMutation如下。
Mutations::UpdateAddressMutation = GraphQL::Relay::Mutation.define do
name 'UpdateAddress'
input_field :addressInput, !Types::AddressInputType # input_fieldにTypes::AddressInputTypeを指定する
return_field :address, !Types::AddressType
resolve ->(_obj, inputs, ctx) {
address_input = inputs.addressInput # input_fieldのネストが一段深くなるので、addressInputを取り出す
begin
address = ctx[:current_user].address
address.postal_code = address_input.postal_code
address.address = address_input.address
address.save
rescue => e
return GraphQL::ExecutionError.new(e.message)
end
{ address: address }
}
end
现在,您可以通过以下方式发送UpdateAddressMutation的查询(与以前有些不同)。
mutation {
updateAddressMutation(input: {addressInput: {postal_code: 1638001, address: "東京都新宿区西新宿2丁目8−1"}}) {
address {
id
postal_code
address
}
}
}
GraphQL的测试
请求规范
写测试来确认GraphQL的行为似乎是编写请求规范(request spec)的好方法。通过在测试中使用与graphiql相同的查询,您可以测试实际的行为。
让我们忽略说明,来看看实施方案。我们将在spec/requests/graphql/query/user_spec.rb中编写以下类似的测试。
require 'rails_helper'
RSpec.describe 'user query', type: :request do
subject { post graphql_path, params: { query: query } }
let!(:user) { FactoryBot.create(:user) }
let(:query) do
<<~QUERY
{
user {
id
email
name
}
}
QUERY
end
it 'response body is User data' do
subject
json = JSON.parse(response.body, symbolize_names: true)
expect(json[:data][:user][:id]).to eq user.id
expect(json[:data][:user][:email]).to eq user.email
expect(json[:data][:user][:name]).to eq user.name
end
end
您可以通过这种方式确认您是否发送了预期的查询。我知道有些人认为这并不是必需的,但我个人认为这是一个必要的测试,并且在以后查看代码时,这一行会让行为立即变得清晰明了,非常方便。您也可以使用相同的方法编写对Mutation的测试。请尝试一下。
检查graphql-ruby的模式是否正确的测试
在graphql-ruby中,可以通过实现来机械输出schema.graphql文件。由于该schema.graphql直接用作文档,只要正确跟进,就不会出现实现和文档脱节的情况。在跟进方面,我认为可以使用CI来自动更新。但在这里,为了简单处理,我将介绍一种方法:如果不是最新的schema,则在测试中将其视为失败。
模式.graphql
在这之前是 schema.graphql。正如之前我说过的,这将成为文档,但为此需要编写说明。在之前的说明中我们忽略了这一点,但在定义 Query 或 Mutation 时需要添加说明。让我们试着添加到 QueryType 和 UserType 中。
Types::QueryType = GraphQL::ObjectType.define do
name 'Query'
field :user, !Types::UserType do
description 'You can access current_user' # description を追加
resolve ->(_obj, _args, ctx) {
ctx[:current_user]
}
end
end
Types::UserType = GraphQL::ObjectType.define do
name 'User'
field :id, !types.ID, 'ユニークな ID'
field :name, !types.String, '名前'
field :email, !types.String, 'e-mail アドレス'
field :address, !Types::AddressType, '住所'
connection :posts, !Types::PostType.connection_type, '投稿一覧'
end
您可以使用块来描述,但目标是相同的。让我们在这种状态下生成schema.graphql。首先,在Rakefile中添加代码以调用graphql-ruby的rake任务。
require_relative 'config/application'
require 'graphql/rake_task' # 追記
Rails.application.load_tasks
GraphQL::RakeTask.new(schema_name: 'GraphqlRubyDemoSchema') # 追記
...
现在我们已经能够通过这个方法dump出schema.graphql文件了,所以我们可以执行rake task。
$ rake graphql:schema:dump
我认为schema.graphql和schema.json已经被转储了。schema.json包含了schema的所有信息,并用json格式进行了详细覆盖(graphiql好像在使用它,但我不太确定)。
当查看”schema.graphql”中的”Query”和”User”类型时,我们可以看到之前提到的”description”被输出了出来。将类型定义和说明结合在一起,并从实现中机械地生成这样一个模式,对于json schema和swagger来说非常方便,我觉得很有用。
无法直接使用schema.graphql进行测试,需要先进行仿真测试。
如果机械上产生的schema不是最新的,那就没有意义。所以我们需要编写一个测试来检查从实现中生成的最新schema与已经生成的schema之间是否有差异,如果不是最新的,则会出现错误。如果发生错误,只需重新导出schema即可。我们将添加以下测试(我是一个懒人,所以我将其添加到spec/requests/graphql/graphql_ruby_demo_schema_spec.rb中)。
require 'rails_helper'
RSpec.describe 'GraphqlRubyDemoSchema' do
let(:current_definition) { GraphqlRubyDemoSchema.to_definition }
let(:printout_definition) { File.read(Rails.root.join('schema.graphql')) }
it 'equals dumped schema, `rake graphql:schema:dump` please!' do
expect(current_definition).to eq(printout_definition)
end
end
请尝试更改一个类型进行测试,并运行测试。如果出现错误,请将其保存并再次运行测试。我认为测试应该会通过。
通过这样做,您一定会意识到模式是过时的,并且可以始终使文档与实施保持一致。同时,您还可以确认是否是您所打算的更改。由于这实际上非常方便,因此请务必尝试使用!
最终
由于GraphQL官方文档非常完善,所以只要阅读它就能找到所有所需信息。虽然有写出来,但由于篇幅相对较多,一开始理解概要确实有些困难。
因此,我觉得如果有一篇能总览全局的文章就好了,于是写了这个。希望能作为初学者的参考!
在中国的本地化语言环境中,以下是原文的再述:
GraphQL-Ruby具有许多其他功能,同时也有多种写法。由于它是一项尚未确立最佳实践的新技术,让我们一起使用并不断优化它!
以下是参考资料
-
- http://graphql.org/learn/
-
- http://graphql-ruby.org/
- https://blog.qnyp.com/2017/06/08/graphql-resources/