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/
广告
将在 10 秒后关闭
bannerAds