使用Ruby on Rails学习GraphQL(graphql-ruby)的入门备忘录
首先
我用Ruby on Rails尝试了一下GraphQL,并写了一篇备忘录。
环境如下所示。
-
- ruby: 3.0.2
-
- rails: 6.1.4
-
- graphql-ruby: 1.12
- graphiql-rails: 1.8.0
GraphQL 是一种查询语言和运行时环境,用于从客户端向服务器请求和响应数据。
GraphQL是一种用于API的查询语言,与REST API不同,具有以下优点。
-
- 1つのエンドポイントのみで呼び出せる
- 欲しい情報のみを指定でき、余分なものを取得しない
安装
-
- graphql-ruby
- graphiql-rails
我们将参考这个进行在Rails中引入GraphQL和GraphiQL。
通过引入GraphiQL,您可以在开发环境中使用图形界面来确认和执行GraphQL的查询和变更设置,以进行测试。
它类似于phpMyAdmin的GraphQL版本。
gem 'graphql'
group :development do
gem 'graphiql-rails'
end
bundle install
rails g graphql:install
在中文中只需要一个选项来改写以下内容:
进行GraphiQL的路由设置。
这样,您只能在本地开发环境中通过http://localhost:3000/graphiql等URL访问GraphiQL。
Rails.application.routes.draw do
post "/graphql", to: "graphql#execute"
# 以下を追加
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql'
end
end
获取数据 (Query)
为了获取数据,需要定义类型(Type)和查询(Query)。
Type的定义
Type用于定义返回数据的结构。
在生成用于Type定义的文件之前,先创建Rails模型并执行迁移,Type会根据该模式自动生成定义。
rails g model User
# migrationの設定
rails db:migrate
rails g graphql:object User
文件被创建,例如会生成以下类似的代码。
module Types
class UserType < Types::BaseObject
field :id, ID, null: false
field :email, String, null: false
field :encrypted_password, String, null: false
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
end
end
然而,Type是用于定义返回响应数据结构的。它不需要与数据库表结构匹配,您可以自由地添加或删除字段。
像密码之类的不应该放在API中的敏感信息,最好是删除相关字段。
查询的定义
我们将使用Query来定义如何获取信息。
Query的定义将写在app/graphql/types/query_type.rb中。
例如,可以编写查询以获取所有用户或获取指定ID的用户。
module Types
class QueryType < Types::BaseObject
field :users, [UserType], null: false
def users
User.all
end
field :user, UserType, null: false do
argument :id, ID, required: true
end
def user(id:)
User.find(id)
end
end
end
尝试使用GraphiQL获取数据
访问GraphiQL页面,将以下内容输入到左侧的输入框中,然后点击播放按钮选择查询,结果将显示在右侧的框中。
在定义时,字段名称使用snake_case进行编写,但在调用API时,请使用camelCase进行编写。
query users {
users {
id
email
createdAt
}
}
query user1 {
user(id: 1) {
id
email
}
}
query user($id: ID!) {
user(id: $id) {
id
email
}
}
{
"id": "1"
}
在下面的查询中,将参数分开写是一种方式。
型名(ID!)后面的!表示它是一个必需参数。
(因为在查询定义中设置了argument :id, ID, required: true)
此外,可以从右上角的Docs中查看查询、变异和类型的定义。
创建、编辑、删除数据(Mutation)
创建、编辑、删除数据等具有副作用的处理将在Mutation中定义。
mutation的定义应该写在app/graphql/types/mutation_type.rb中。
但是,通过命令生成mutation后,mutation_type.rb文件将自动添加代码,实际编辑的文件将是各个mutation定义文件。
rails g graphql:mutation CreateUser
module Types
class MutationType < Types::BaseObject
field :create_user, mutation: Mutations::CreateUser # 自動で追記される
# ~
end
end
我们将如下定义mutation:
使用field定义响应、argument定义参数,使用resolve定义处理。
module Mutations
class CreateUser < BaseMutation
field :user, Types::UserType, null: false
argument :email, String, required: true
argument :password, String, required: true
argument :password_confirmation, String, required: true
def resolve(**args)
user = User.create!(args)
{ user: user }
end
end
end
试着使用GrapiQL创建数据。
通过调用mutation,可以进行以下操作。
与数据获取操作不同的是,mutation以createUser()开头,但是createUser(email: ~)变为了createUser(input:{email: ~})的结构。
mutation createUser($email: String!, $password: String!, $passwordConfirmation: String!) {
createUser(
input: {email: $email, password: $password, passwordConfirmation: $passwordConfirmation}
) {
user {
id
email
}
}
}
{
"email": "test2@example.com",
"password": "abc123",
"passwordConfirmation": "abc123"
}
编辑和删除
只需按照数据创建时的方法创建变异即可进行编辑或删除。
其他
联想
如果用户拥有多个任务数据,且想要同时获取用户信息和用户拥有的任务的方法。
我会创建一个名为Task的模型,并执行迁移和创建类型,同时设置模型的关联(用户模型和任务模型)。只需在用户类型的定义中添加tasks字段即可。
module Types
class UserType < Types::BaseObject
# ~
field :tasks, [TaskType], null: false
end
end
这样一来,相关模型的信息也能一次性获取。
query user1 {
user(id: 1) {
id
email
tasks {
id
}
}
}
关联模型个数的字段添加,关于N+1问题。
当需要获取用户拥有的任务数量时,可以使用以下方法。
选项1: 将”取得方法”包括在Type定义中的方法
可以在 object 上编写对模型实例的处理。
module Types
class UserType < Types::BaseObject
# ~
field :tasks_count, Int, null: false
def tasks_count
object.tasks.size
end
end
end
为了避免N+1问题,在查询定义的部分要包含(:tasks)。
即使包含了,如果在Type定义中使用object.tasks.count,仍然会导致N+1问题的发生。
module Types
class QueryType < Types::BaseObject
# ~
field :users, [UserType], null: false
def users
User.includes(:tasks)
end
# ~
end
end
选项2:在查询的定义中获取的方法。
module Types
class UserType < Types::BaseObject
# ~
field :tasks_count, Int, null: false
end
end
module Types
class QueryType < Types::BaseObject
# ~
field :users, [UserType], null: false
def users
User.select(%|
*,
(SELECT COUNT (*) FROM tasks t
WHERE t.user_id = users.id
) AS tasks_count
|)
end
# ~
end
end
尽管我更喜欢这种生成的SQL语句,并且性能可能更好,但是老实说,考虑到可读性以及需要针对每个查询和变异分别编写,我觉得这种方法可能有些微妙。
其他的方法 de
我还没有尝试过,但使用名为graphql-batch的gem可以更灵活地获取数据,而不会出现N+1问题?
会话、当前用户
在Query或Mutation的定义中,不能直接调用像session或devise的current_user这样的辅助方法。要能够调用它们,需要在GraphqlController中进行设置。
class GraphqlController < ApplicationController
# ~
def execute
# ~
context = {
session: session,
current_user: current_user,
}
# ~
end
# ~
end
设定如下,这样在查询或者变更内可以通过调用context[:session]或者context[:current_user]进行调用。
最后
这次是我第一次尝试使用GraphQL,我能够轻松地实现基本的数据获取、创建、编辑和删除等功能。然而,在解决N+1问题以及认证和授权方面,虽然本次文章没有写到,但我在实践过程中遇到了许多困难。因为我仍然对许多方面都没有完全理解,所以我希望能够继续学习GraphQL。
感谢您一直阅读到最后。
我参考了那篇文章。
-
- 入門 GraphQL Ruby 〜基本のCRUD処理〜
- 【Rails】graphql-rubyでAPIを作成