使用graphql-ruby后的感受和学到的东西

好久不见我来发帖了。

最近我在业务中尝试了使用graphql-ruby,对此有了一些感触和学到的东西,我想总结一下。
由于这是我第一次使用GraphQL,一开始有点困惑,但是试用之后觉得非常方便,感觉有很多好处,因此以后在API选择上,我希望GraphQL成为首选。

环境

Ruby 2.5.1
GraphQL 1.8.7
Ruby 2.5.1
GraphQL 1.8.7

学到的知识,所感受的。

确保自己能够正确阅读所使用版本的文档。

很明显的一件事情是,这非常重要。

顺便提一下,文件在这里。

我使用的是最新版本的「1.8.7」,该版本是在2018年8月推出的。从1.8版开始,它转变为基于类的API,这对我来说确实造成了一些困扰。
顺便提一下,1.8版是在2018年5月18日发布的。

具体来说,包括Query和Mutation等的编写方式有相当大的变化。

# 1.7以前

## Mutation
MutationType = GraphQL::ObjectType.define do
  name "Mutation"
  field :createPost, types.Post do
    resolve ->(obj, args, ctx) {
      obj # => #<Organization id=456 ...>
      # ...
    }
  end
end

## Schema定義
ObjectTypes::User = GraphQL::ObjectType.define do
  name "User"
  field :id, !types.ID
  field :name, types.String
  field :posts, ObjectTypes::PostList do
    resolve ->(obj, args, ctx) {
      Post.where(...)
    }
  end
end

# 1.8以降

## Mutation
class MutationType < GraphQL::Schema::Object
  field :create_post, Post, null: true
  def create_post(**args)
    object # => #<Organization id=456 ...>
    # ...
  end
end

## Schema定義
class UserType < GraphQL::Schema::Object
  field :posts, [ObjectTypes::PostType], null: true

  def posts
    Post.where(...)
  end
end

仅仅是升级了一个小版本,却变化如此之大……顺便提一下,在2018年8月,市面上发布的文章仅到1.7版本,如果参考那些文章就会非常困惑。

当发现在次要版本中存在如此大的变更的例子时,可以很有可能在1.9版本中也会发生变化,所以阅读使用版本的文档非常重要。

然而,即使在最新的文件示例中,也有零星出现1.7的描述,但也可能没有。如果您对英语有信心,我认为提交PR是可以通过的。

在Rails项目中,GraphiQL是必不可少的。

这已经是必须的了。
只需安装graphiql-rails,就能使用Graphiql,使开发速度大大提升。
以下是GitHub公开的Graphiql。尝试使用后,会更清楚它的便利之处。

Graphiql的示例→https://developer.github.com/v4/explorer/

如果你打算使用graphql-ruby进行开发,首先请安装grapiql-rails。

如果平常写作的话,很容易出现N+1问题。

满心对GraphQL的便利感欣喜若狂时,不小心编写出了一段糟糕的代码,导致了以下问题的发生。例如,当发送以下查询时,必然会出现N+1问题。

{
  user {
    id
    name
    posts {
      edges {
        node {
          id
          title
        }
      }
    }
  }
}

在获取posts时,可能会出现N+1的情况。

这个问题在很多地方被讨论,有人建议使用graphql-batch,或者在查询的字段定义中使用includes等等不同的方法被提出。

Q: 如何防止N+1查询?
在graphql-ruby中如何处理N+1查询?

目標とする機能を最初に実装し、後で必要に応じて追加することにしました。

    • 確実にQueryで取得すると分かっているものは、eager_loadで結合する

 

    それ以外のものは、includesを使って、呼び出されるときに、N+1が起きないようにする

我选择了这种方法。这个方法既能节省时间,又能大幅减少SQL查询次数。特别是在开发初期,我们能预测到所有会发出的查询。

我认为可能在发布后,如果可以的话,我想做以下的事情,以提高性能。

同时兼具简洁性和性能的API设计与实现的一个示例

将文件按类型分成不同的目录

我参考了以下的文章,并按照目录将文件分开。

参考:在创建Rails的GraphQL API时,我遇到了5个困难。

公式文件建议创建一个Base类,但是我们将所有的Base类放在了type目录下面。然而,随着模型数量的增加,不同的枚举类型、标量类型和对象类型混合在一起,使得文件变得混乱不易阅读。

因此,我预先根据类型将目录分开了。基本上,我试着按照上述参考文章的方式进行了分类。

app/graphql/enum_types/
           /input_types/
           /interface_types/
           /mutations/
           /object_types/
           /scalar_types/

几乎与参考文章一致,只是在input_types方面,不仅考虑了可重复使用性,也考虑了易读性,所以我们希望将希望在Mutation中进行更新的内容整理到InputTypes中,如下所示。

module InputTypes
  class CourseInputType < InputTypes::BaseInputObject
    graphql_name 'CourseInputType'

    argument :name, String, required: true
    argument :content, String, required: true
    argument :price, Integer, required: false
    #...
  end
end

当然,我们也在考虑到可再利用性,但我主要是以易读性为考量来总结的。

在处理错误的功能方面

关于这个问题,意见可能会有分歧,但我按照官方文件的规定,决定返回errors字段。

具体来说,如下所述(摘自官方文件)。

def resolve(id:, attributes:)
  post = Post.find(id)
  if post.update(attributes)
    {
      post: post,
      errors: [],
    }
  else
    # Convert Rails model errors into GraphQL-ready error hashes
    user_errors = post.errors.map do |attribute, message|
      # This is the GraphQL argument which corresponds to the validation error:
      path = ["attributes", attribute.camelize]
      {
        path: path,
        message: message,
      }
    end
    {
      post: post,
      errors: user_errors,
    }
  end
end

由于GraphQL的特性,即使返回错误代码,也无法确定错误发生的位置,因此基本上返回200,并将错误内容返回给errors。我们也参考了以下文章。

在创建GraphQL API时,我遇到了5个困惑。参考:在Rails上创建GraphQL API时的5个疑问。

在每个查询和突变中编写测试。

这个问题可能会引发很多不同的意见,但是我总结出了如下的结论。
在官方文档中,也详细说明了如何编写规格说明书。

执行GraphQL查询

我正在执行Query测试(摘自官方文档)。

RSpec.describe MySchema do
  # You can override `context` or `variables` in
  # more specific scopes
  let(:context) { {} }
  let(:variables) { {} }
  # Call `result` to execute the query
  let(:result) {
    res = MySchema.execute(
      query_string,
      context: context,
      variables: variables
    )
    # Print any errors
    if res["errors"]
      pp res
    end
    res
  }

  describe "a specific query" do
    # provide a query string for `result`
    let(:query_string) { %|{ viewer { name } }| }

    context "when there's no current user" do
      it "is nil" do
        # calling `result` executes the query
        expect(result["data"]["viewer"]).to eq(nil)
      end
    end

    context "when there's a current user" do
      # override `context`
      let(:context) {
        { current_user: User.new(name: "ABC") }
      }
      it "shows the user's name" do
        user_name = result["data"]["viewer"]["name"]
        expect(user_name).to eq("ABC")
      end
    end
  end
end

将顶级的describe命名为查询名称,并更改为更易理解的名称。
同时,针对查询和变更操作将文件进行分离,并按以下结构创建文件。

spec/request/graphql/query/course_spec.rb
                           user_spec.rb
                    /mutation/create_course_spec.rb
                              create_user_spec.rb

综述

起初有一些困惑的地方,但是一旦开始使用,就会发现非常方便。而且随着熟悉开发,由于graphiql-rails的帮助,速度也变得非常快。

如果想要使用Rails开发API,我觉得可以考虑首先选择GraphQL作为一个选择。

我把它们一一列出,希望能对以后使用graphql-ruby进行开发的人有所帮助。

广告
将在 10 秒后关闭
bannerAds