使用Ruby on Rails学习GraphQL的最新版本(2018年)
首先
这篇文章是作者目前在实际工作中使用GraphQL的经验总结。
虽然已经公开了,但是每天都会进行追赶并随时更新,希望您能善意地关注!
如果在所记录的文章中存在任何错误或不足之处,请您提出指正,将不胜感激。
目标读者
-
- Class_BaseにGraphQLの書き方を知りたい人
-
- GraphQLでのMutationの書き方がいまいちわからない人
- とりあえず、RailsとGraphQLでCRUDをやってみたい人
只需要一个选项
GraphQL是什么?概念性的内容不太涉及。
有关这些内容,请参考其他网站或文章中的大量介绍,参考链接请查阅。
请提供参考链接
GraphQL与REST的区别
往 Gemfile 中添加 graphql。
gem 'graphql'
生成GraphQL
rails g graphql:install
文件组将按照以下方式创建。
Running via Spring preloader in process 75061
create app/graphql/types
create app/graphql/types/.keep
create app/graphql/bookshelf_schema.rb
create app/graphql/types/base_object.rb
create app/graphql/types/base_enum.rb
create app/graphql/types/base_input_object.rb
create app/graphql/types/base_interface.rb
create app/graphql/types/base_union.rb
create app/graphql/types/query_type.rb
add_root_type query
create app/graphql/mutations
create app/graphql/mutations/.keep
create app/graphql/types/mutation_type.rb
add_root_type mutation
create app/controllers/graphql_controller.rb
route post "/graphql", to: "graphql#execute"
Skipped graphiql, as this rails project is API only
You may wish to use GraphiQL.app for development: https://github.com/skevy/graphiql-app
使用brew来安装GraphiQL。
公式网页:https://electronjs.org/apps/graphiql
使用brew,在Mac上安装GraphiQL。
brew cask install graphiql
在GraphiQL中尝试发出查询
将以下的URL输入到GraphQL终端点中。
请创造一个选项,以中文本土翻译以下内容:http://api.graphloc.com/graphql
接下来,我们尝试查询纬度和经度,并进行操作确认。
{
getLocation(ip: "8.8.8.8") {
location {
latitude
longitude
}
}
}
成功后,会返回如下数据。
{
"data": {
"getLocation": {
"location": {
"latitude": "37.751",
"longitude": "-97.822"
}
}
}
}
使用GrahiQL进行操作。
如果Rails服务器已经启动应用程序服务器,那么可以在GraphiQL中运行它。
如果启动正确,则点击将打开文档浏览器,以便能够打开“Docs”。
点击Query按钮,可以查看当前项目中在query_type.rb文件中定义的查询。
关于JSON,请参考以下的维基百科。
query_type的写法
执行 rails g graphql:install 命令后,会生成以下样式的 query_type.rb 文件。
module Types
class QueryType < Types::BaseObject
# Add root-level fields here.
# They will be entry points for queries on your schema.
# TODO: remove me
field :test_field, String, null: false,
description: "An example field added by the generator XYZ"
def test_field
"Hello World!"
end
end
如果要解释所显示的项目
大概就是这样。
如果你想在filed中定义参数的话
作为写作方法,首先创建一个块(block),然后在其中定义参数作为参数引用。
在定义时,如果是必需的数据类型,应标明required:true。
只需要按照通常方法将参数传递给方法即可。
module Types
class QueryType < Types::BaseObject
field :test_field, String, null: false,
description: "An example field added by the generator XYZ"do
argument :name, String, required: true
end
# フロント側で呼び出すfieldをメソッド形式を定義
def test_field(name:)
"Hello #{name}"
end
end
end
当你重新加载 GraphiQL 并确认时,你会发现参数中新增了 name。
如果在testField的参数中指定name,并添加适当的字符串,就可以看到它正确地反映了定义。
关于上下文的问题
context在这里的作用是将每个查询的特定信息以哈希形式传递给后面提到的resolve方法。
当打开graphql_controller.rb文件时,可以看到以下代码。
class GraphqlController < ApplicationController
def execute
variables = ensure_hash(params[:variables])
query = params[:query]
operation_name = params[:operationName]
context = {
time: Time.now
# Query context goes here, for example:
# current_user: current_user,
}
result = BookshelfSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
render json: result
rescue => e
raise e unless Rails.env.development?
handle_error_in_development e
end
private
# Handle form data, JSON body, or a blank value
def ensure_hash(ambiguous_param)
case ambiguous_param
when String
if ambiguous_param.present?
ensure_hash(JSON.parse(ambiguous_param))
else
{}
end
when Hash, ActionController::Parameters
ambiguous_param
when nil
{}
else
raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
end
end
def handle_error_in_development(e)
logger.error e.message
logger.error e.backtrace.join("\n")
render json: { error: { message: e.message, backtrace: e.backtrace }, data: {} }, status: 500
end
end
这段中有一个context = {}的地方,在那里定义了想要传递给resolve方法的context。
(省略)
context = {
time: Time.now
}
(省略)
接着我们在query_type.rb中将context[:time]传递进去。
(省略)
def test_field(name:)
Rails.logger.info context[:time]
"Hello #{name}!"
end
(省略)
在这种情况下,通过GraphiQL发送查询并查看Rails日志。
Started POST "/graphql" for 127.0.0.1 at 2018-09-16 00:13:18 +0900
Processing by GraphqlController#execute as HTML
Parameters: {"query"=>"{\n testField(name: \"Ruby\")\n}", "variables"=>nil, "graphql"=>{"query"=>"{\n testField(name: \"Ruby\")\n}", "variables"=>nil}}
2018-09-16 00:13:18 +0900 #=> context = { time: Time.now }で定義した部分
Completed 200 OK in 43ms (Views: 0.7ms | ActiveRecord: 0.0ms)
我能看到现在的时间显示。
只需要一个选项,以下是一种中文翻译:
例如,如果传递一个表示当前登录用户的current_user,就可以通过日志进行确认。
关于标量
标量是在GraphQL中以自定义数据类型来管理每个对象的键和值的东西。
GraphQL默认提供了以下5种标量类型。
然而,由于可以自定义标量类型,所以可以应用这一特性来进行定制。
关于Null约束
在GraphQL中,可以为每个字段设置null约束。
当在字段中设置了 null 或 false,前端在获取查询或执行变更时将会返回错误。
如果你想在服务器端也设定null限制条件,只需要另外定义验证规则即可。
自己定义查询类型
可以使用ActiveRecord来定义自己的Query_type_w。
如果想要自定义一个名为full_name的方法,只需在相应的模型中进行定义,并将该field添加到query_type中即可。
# author.rb
class Author < ApplicationRecord
def full_name
([first_name, last_name].compact).join " "
end
end
# author_type.rb
class Types::AuthorType < Types::BaseObject
(省略)
field :full_name, String, null: false, camelize: false
end
顺便说一下,即使不在模型中编写逻辑,也可以在每个query_type.rb文件中进行编写,能得到相同的结果。
在这种情况下,只需将对象定义的字段名前缀添加对象即可,即可满足要求。
(GraphQL会自动检测对象的模型名称作为其对象)
# author_type.rb
class Types::AuthorType < Types::BaseObject
(省略)
field :full_name, String, null: false, camelize: false
def full_name
([object.first_name, object.last_name]).compact).join " "
end
end
当在GraphiQL中实际测试时,可以发现定义的字段正常返回响应。
自定义fieldType。
在GraphQL中,可以自定义字段类型。
只需要一种选择:
方法很简单,
-
- 在模型中通过定义的方法来定义想要的fieldType。
-
- 在graphql/types目录下创建一个新的query_type.rb,与第一条相同的名称。
-
- 定义获取field所需的字段。
- 在与模型相应的各个query_type.rb中添加field。
在模型中定义一个自己想要的fieldType,将其定义为一个方法。
例如,在模型文件中定义一个名为”coordinates”的FieldType方法,用于创建纬度和经度。
class Author < ApplicationRecord
def coordinates
# 緯度と経度について、それぞれ定義するメソッド
# 第一要素が緯度で第二要素が経度
[rand(90), rand(90)]
end
end
在graphql/types的目录下创建一个名为query_type.rb的新文件,与1相同的名称。
touch app/graphql/types/coordinates_type.rb
定义在根级别上获取的字段。
# coordinates_type.rb
class Types::CoordinatesType < Types::BaseObject
field :latitude, Float, null: false
field :longitude, Float, null: false
def latitude
object.first
end
def longitude
object.last
end
end
在每个对应于模型的query_type.rb文件中添加字段
# author_type.rb
class Types::AuthorType < Types::BaseObject
field :coordinates, Types::CoordinatesType, null: false
end
使用QueryType来定义一个数组。
首先,要定义一个数组,需要在模型中定义一个方法来定义字段,该模型是数据库的入口。
# author.rb
class Author < ApplicationRecord
def publication_years
# 出版した年度が配列でランダムに最大10個返ってくる
(1..rand(10)).to_a.map { 1900 - rand(100)}
end
end
接下来,在定义每个查询类型的文件中,定义字段。
class Types::AuthorType < Types::BaseObject
field :publication_years, [Int], null: true
end
最后,在query_type.rb的根级别定义field和Array的调用方法。
field :authors, [Types::AuthorType], null: false
def authors
Author.all
end
当使用这个方法查询GraphiQL时,如果将其变为复数形式,就可以通过数组来获取查询结果。
由于变异导致的CRUD操作
Mutation是在修改数据时使用的字段。
在更改现有字段时使用,类似于创建、更新和删除操作。
通过突变实现创建
Mutation进行创建操作的方法有两种。
把以下内容写在types文件夹中的mutation_type.rb文件中。
# app/graphql/types/mutation_type.rb
module Types
class MutationType < Types::BaseObject
field :create_author, AuthorType, null: true, description: "Create an author" do
argument :first_name, String, required: false, camelize: false
argument :last_name, String, required: false, camelize: false
argument :yob, Int, required: false
argument :is_alive, Boolean, required: false, camelize: false
end
def create_author(first_name:, last_name:, yob:, is_alive:)
Author.create first_name: first_name, last_name: last_name, yob: yob, is_alive: is_alive
end
end
end
创建一个处理字段(field)并在块中描述传递给参数的处理。
field :create_author, AuthorType, null: true, description: "Create an author" do
argument :first_name, String, required: false, camelize: false
argument :last_name, String, required: false, camelize: false
argument :yob, Int, required: false
argument :is_alive, Boolean, required: false, camelize: false
end
然后,以方法的形式定义用于向数据库查询的ActiveRecord处理。
def create_author(first_name:, last_name:, yob:, is_alive:)
Author.create first_name: first_name, last_name: last_name, yob: yob, is_alive: is_alive
end
在”mutations”目录下创建一个名为”mutation.rb”的文件,并进行描述。
class Mutations::CreateAuthor < GraphQL::Schema::Mutation
null true
argument :first_name, String, required: false, camelize: false
argument :last_name, String, required: false, camelize: false
argument :yob, Int, required: false
argument :is_alive, Boolean, required: false, camelize: false
def resolve(first_name:, last_name:, yob:, is_alive:)
Author.create first_name: first_name, last_name: last_name, yob: yob, is_alive: is_alive
end
end
在mutations目录中创建一个mutation.rb文件,并定义在进行create操作时所需要的参数。
argument :first_name, String, required: false, camelize: false
argument :last_name, String, required: false, camelize: false
argument :yob, Int, required: false
argument :is_alive, Boolean, required: false, camelize: false
然后,定义resolve方法,并传递所需字段作为参数,编写处理代码以向ActiveRecord查询数据库。
def resolve(first_name:, last_name:, yob:, is_alive:)
Author.create first_name: first_name, last_name: last_name, yob: yob, is_alive: is_alive
end
最后,在types文件夹中调用了在mutation_type.rb文件中定义的mutation,它的行为和第一个选项相同。
module Types
class MutationType < Types::BaseObject
field :create_author, Types::AuthorType, mutation: Mutations::CreateAuthor
end
end
关于查询变量(Query_Variable),请提供更多信息。
使用GraphiQL执行Mutation时,可以使用query_variable,在另一个窗口中定义field的值,非常方便。
mutation createAuthor($first_name:String, $last_name:String, $yob:Int, $is_alive:Boolean){
createAuthor(first_name:$first_name, last_name:$last_name, yob:$yob, is_alive:$is_alive) {
id
full_name
}
}
在mutation后面写下Mutation的名称,并在field名称前面加上$作为前缀。
createAuthor($first_name:String, $last_name:String, $yob:Int, $is_alive:Boolean)
接下来,我们需要将带有”$”符号的字段名称传递给mutation的部分。
createAuthor(first_name:$first_name, last_name:$last_name, yob:$yob, is_alive:$is_alive)
只需在GraphiQL的QUERY VARIABLES部分以JSON格式编写相应的字段名称及所需的值即可。
{
"first_name": "Yammy",
"last_name": "Humberg",
"yob": 2019,
"is_alive": true
}
当成功时,会出现以下的回应。
{
"data": {
"createAuthor": {
"id": "7",
"full_name": "Yammy Humberg"
}
}
}
关于InputTypes
迄今为止,在执行Create操作时,我们一直在GraphiQL中这样写:
mutation createAuthor($first_name:String, $last_name:String, $yob:Int, $is_alive:Boolean){
createAuthor(first_name:$first_name, last_name:$last_name, yob:$yob, is_alive:$is_alive) {
id
full_name
}
}
然而,如果出现了两次相同的字段名称,完全不符合DRY原则。
因此,登场的是InputType。
InpuType 类继承于 GraphQL::Schema::InputObject,并按照以下方式进行定义。
class Types::AuthorInputType < GraphQL::Schema::InputObject
graphql_name "AuthorInputType"
description "All the attributes for creating an author"
argument :id, ID, required: false
argument :first_name, String, required: false, camelize: false
argument :last_name, String, required: false, camelize: false
argument :yob, Int, required: false
argument :is_alive, Boolean, required: false, camelize: false
end
关于InputType的各项内容如下所示。
接下来,我们将使用InputType将在MutationType中定义的Create重新编写。
class Types::MutationType < Types::BaseObject
field :create_author, AuthorType, null: true, description: "Create an author" do
argument :author, Types::AuthorInputType, required: true
end
def create_author(author:)
Author.create author.to_h
end
end
只需在GraphiQL中按如下方式提交查询,然后在QUERY VARIABLES中指定值即可。
查询
mutation createAuthor($author:AuthorInputType!) {
createAuthor(author:$author) {
id
full_name
}
}
查询变量
{ "author": {
"first_name": "Hina",
"last_name": "Okamoto",
"yob": 1933,
"is_alive": true
}
}
通过突变进行的更新。
如果可以做到输入类型(InputType),其他就很简单了。只需单独创建更新(Update)的变更(Mutation)即可。
class Types::MutationType < Types::BaseObject
field :update_author, Boolean, null: false, description: "Update an author" do
argument :author, Types::AuthorInputType, required: true
end
def update_author(author:)
existing = Author.where(id: author[:id]).first
#&.でnil以外なら更新されたデータのhashを返す
existing&.update author.to_h
end
也许有人对在这里将update_author指定为布尔类型感到疑惑。
因为前端不需要知道数据库发生了什么样的变化,所以基本上不需要传递ApplicationRecord的返回值,这是可以的。
因此,前端只需将其设置为布尔类型,并返回update是true还是false,因此就是这样。
由于变异而删除
只需传递要删除的ID作为参数即可,而不需要使用InputType进行删除。
class Types::MutationType < Types::BaseObject
field :delete_author, Boolean, null: false, description: "Delete an author"do
argument :id, ID, required: true
end
def delete_author(id:)
Author.where(id: id).destroy_all
true
end
end
通过destroy_all方法,使用ActiveRecord来查询数据库中的ID,并删除相应的记录。