使用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进行开发的人有所帮助。