我尝试在Rails 7的API模式下使用GraphQL
目标
确认在使用 Rails 和 GraphQL 的时候,写作会是什么样子的。
必需品 (bì xū
-
- Docker(今回使用バージョン: Docker version 20.10.12, build e91ed57)
-
- VSCode(Remote Develpment 拡張機能を使用, 今回使用バージョン: Version: 1.64.1)
- ブラウザ(今回は Chrome バージョン: 98.0.4758.80(Official Build) を使用)
搭建开发环境
使用VSCode启动并创建Dockerfile来启动虚拟环境。
在VSCode中打开一个用于开发的空文件夹(例如,这里设为 /dev/rails_graphql)。
创建一个适用于 Ruby 环境的 Dockerfile。
FROM ruby
使用VSCode的”Reopen in Container”命令,可以从Dockerfile切换到虚拟环境。
创建用于安装 Rails 的 Gemfile。
source "https://rubygems.org"
gem 'rails'
打开 VSCode 的终端并安装 gem。
bundle install
确认Ruby和Rails的版本
ruby -v
ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [x86_64-linux]
rails -v
Rails 7.0.2.2
创建一个Rails项目
rails new --api --minimal .
确认行动
启动服务器
rails s
如果访问http://localhost:3000,您可以确认Rails正在运行。

添加适用于 GraphQL 的宝石 (gem)。
bundle add graphql
- bundle add --group development graphiql sass-rails
+ bundle add --group development graphiql-rails sass-rails
※ graphiql 是用于开发的工具,而 sass-rails 是 graphiql 运行所必需的。
生成与graphql相关的文件
rails g graphql:install
调整配置以使GraphiQL运行
启用会话
在 application.rb 文件中添加了两行代码。
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore
在路径上添加设置。
if Rails.env.development?
mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
end
确认行动.
在终端运行 `rails s` 启动服务器,然后访问 http://localhost:3000/graphiql。

执行为测试生成的查询

创建GraphQL查询
我們這次打算製作一個預計使用活動日程表的API。
首先,我们创建一个用于管理事件日程的模型和REST API。
rails g scaffold Event title:string:index start_at:datetime:index end_at:datetime place:string:index tags:string memo:text canceled:boolean:index
invoke active_record
create db/migrate/20220213115441_create_events.rb
create app/models/event.rb
invoke test_unit
create test/models/event_test.rb
create test/fixtures/events.yml
invoke resource_route
route resources :events
invoke scaffold_controller
create app/controllers/events_controller.rb
invoke resource_route
invoke test_unit
create test/controllers/events_controller_test.rb
下一步我们要生成参与者列表
rails g scaffold Member name:string:index email:string:uniq
invoke active_record
create db/migrate/20220213115747_create_members.rb
create app/models/member.rb
invoke test_unit
create test/models/member_test.rb
create test/fixtures/members.yml
invoke resource_route
route resources :members
invoke scaffold_controller
create app/controllers/members_controller.rb
invoke resource_route
invoke test_unit
create test/controllers/members_controller_test.rb
最后生成参加活动的人数。
rails g scaffold EventMember event:references member:references presented:boolean:index
invoke active_record
create db/migrate/20220213115822_create_event_members.rb
create app/models/event_member.rb
invoke test_unit
create test/models/event_member_test.rb
create test/fixtures/event_members.yml
invoke resource_route
route resources :event_members
invoke scaffold_controller
create app/controllers/event_members_controller.rb
invoke resource_route
invoke test_unit
create test/controllers/event_members_controller_test.rb
将非空值或默认值添加到设置中。
class CreateEvents < ActiveRecord::Migration[7.0]
def change
create_table :events do |t|
t.string :title, null: false
t.datetime :start_at, null: false
t.datetime :end_at
t.string :place
t.string :tags
t.text :memo
t.boolean :canceled, null: false, default: false
t.timestamps
end
add_index :events, :title
add_index :events, :start_at
add_index :events, :place
add_index :events, :canceled
end
end
class CreateMembers < ActiveRecord::Migration[7.0]
def change
create_table :members do |t|
t.string :name, null: false
t.string :email, null: false
t.timestamps
end
add_index :members, :name
add_index :members, :email, unique: true
end
end
class CreateEventMembers < ActiveRecord::Migration[7.0]
def change
create_table :event_members do |t|
t.references :member, null: false, foreign_key: true
t.boolean :presented, null: false, default: false
t.timestamps
end
add_index :event_members, :presented
end
end
我們執行遷移。
rails db:migrate
== 20220213115441 CreateEvents: migrating =====================================
-- create_table(:events)
-> 0.0098s
-- add_index(:events, :title)
-> 0.0016s
-- add_index(:events, :start_at)
-> 0.0016s
-- add_index(:events, :place)
-> 0.0015s
-- add_index(:events, :canceled)
-> 0.0015s
== 20220213115441 CreateEvents: migrated (0.0166s) ============================
== 20220213115747 CreateMembers: migrating ====================================
-- create_table(:members)
-> 0.0125s
-- add_index(:members, :name)
-> 0.0014s
-- add_index(:members, :email, {:unique=>true})
-> 0.0015s
== 20220213115747 CreateMembers: migrated (0.0157s) ===========================
== 20220213115822 CreateEventMembers: migrating ===============================
-- create_table(:event_members)
-> 0.0138s
-- add_index(:event_members, :presented)
-> 0.0013s
== 20220213115822 CreateEventMembers: migrated (0.0154s) ======================
生成GraphQL类型
rails g graphql:object Event
create app/graphql/types/event_type.rb
从 events 表中获取信息并自动生成,你可以这样理解。
# frozen_string_literal: true
module Types
class EventType < Types::BaseObject
field :id, ID, null: false
field :title, String, null: false
field :start_at, GraphQL::Types::ISO8601DateTime, null: false
field :end_at, GraphQL::Types::ISO8601DateTime
field :place, String
field :tags, String
field :memo, String
field :canceled, Boolean, null: false
field :created_at, GraphQL::Types::ISO8601DateTime, null: false
field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
end
end
我們也會生成 member 和 event_member 的部分。
rails g graphql:object Member
create app/graphql/types/member_type.rb
rails g graphql:object EventMember
create app/graphql/types/event_member_type.rb
添加一个可以获取所有事件的GraphQL查询。
field :events, [Types::EventType], null: false
def events
Event.all
end
在GraphiQL中进行操作验证
运行rails s命令启动服务器,然后访问http://localhost:3000/graphiql。

执行查询以获取事件的id,标题和开始时间
{
events {
id
title
startAt
}
}

由于还没有任何数据,所以它是一个空数组。
在Rails c中输入测试数据
rails c
Event.create! title: '勉強会', start_at: '2022/02/18 19:00', end_at: '2022/02/18 21:00'
(13.6ms) SELECT sqlite_version(*)
TRANSACTION (0.1ms) begin transaction
Event Create (19.8ms) INSERT INTO "events" ("title", "start_at", "end_at", "place", "tags", "memo", "canceled", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) [["title", "勉強会"], ["start_at", "2022-02-18 19:00:00"], ["end_at", "2022-02-18 21:00:00"], ["place", nil], ["tags", nil], ["memo", nil], ["canceled", 0], ["created_at", "2022-02-13 13:05:15.499816"], ["updated_at", "2022-02-13 13:05:15.499816"]]
TRANSACTION (45.7ms) commit transaction
=>
#<Event:0x00007f680970b048
id: 1,
title: "勉強会",
start_at: Fri, 18 Feb 2022 19:00:00.000000000 UTC +00:00,
end_at: Fri, 18 Feb 2022 21:00:00.000000000 UTC +00:00,
place: nil,
tags: nil,
memo: nil,
canceled: false,
created_at: Sun, 13 Feb 2022 13:05:15.499816000 UTC +00:00,
updated_at: Sun, 13 Feb 2022 13:05:15.499816000 UTC +00:00>
再次执行 GraphQL 查询。

你投入的数据会返回来的。
我要创建一个用于注册的突变。
生成Mutation用的文件。
rails g graphql:mutation CreateEvent
create app/graphql/mutations/create_event.rb
已生成以下文件。
module Mutations
class CreateEvent < BaseMutation
# TODO: define return fields
# field :post, Types::PostType, null: false
# TODO: define arguments
# argument :name, String, required: true
# TODO: define resolve method
# def resolve(name:)
# { post: ... }
# end
end
end
我将对此进行修正。(Wǒ duì cǐ .)
module Mutations
class CreateEvent < BaseMutation
field :event, Types::EventType, null: false
argument :title, String, required: true
argument :start_at, GraphQL::Types::ISO8601DateTime, required: true
argument :end_at, GraphQL::Types::ISO8601DateTime, required: false
argument :place, String, required: false
argument :tags, String, required: false
argument :memo, String, required: false
def resolve(**args)
{ event: Event.create!(**args) }
end
end
end
对Mutation进行操作确认
在浏览器中执行突变。
mutation {
createEvent(
input:{
title: "合宿"
startAt: "2022-02-26T09:00:00Z"
endAt: "2022-02-27T18:00:00Z"
place: "有馬温泉"
tags: "Ruby 合宿 温泉"
}
){
event {
id
title
}
}
}

我试着查询一下注册内容。
{
events {
id
title
startAt
endAt
place
}
}

你已经成功注册了呢。
添加适用于会员的查询和变更操作
我将添加以下查询
field :members, [Types::MemberType], null: false
def members
Member.all
end
我会在浏览器中确认查询内容。
{
members {
id
name
email
}
}

我們將添加突變。
rails g graphql:mutation CreateMember
create app/graphql/mutations/create_member.rb
修正 Mutation 文件。
module Mutations
class CreateMember < BaseMutation
field :member, Types::MemberType, null: false
argument :name, String, required: true
argument :email, String, required: true
def resolve(**args)
{ member: Member.create!(**args) }
end
end
end
在浏览器中检查突变。
mutation {
createMember(
input:{
name: "山田 太郎"
email: "yamada.taro@example.com"
}
){
member {
id
name
email
}
}
}

创建参与活动的查询和变更。
创造活动参与者的查询和变更。
由于事件(Event)和成员(Member)之间存在多对多的关系,因此需要先在模型类(Model)中设置关联。
class Event < ApplicationRecord
has_many :event_members
has_many :members, through: :event_members
end
class Member < ApplicationRecord
has_many :event_members
has_many :events, through: :event_members
end
在将数据插入event_members之前,请先准备好数据。
rails c
Member.create! name: '鈴木 花子', email: 'suzuki.hanako@example.com'
Event.first.then{|event| Member.all.each {|member| event.event_members.create! member: }}
Event Load (2.0ms) SELECT "events".* FROM "events" ORDER BY "events"."id" ASC LIMIT ? [["LIMIT", 1]]
Member Load (2.0ms) SELECT "members".* FROM "members"
TRANSACTION (0.1ms) begin transaction
EventMember Create (21.4ms) INSERT INTO "event_members" ("event_id", "member_id", "presented", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["event_id", 1], ["member_id", 1], ["presented", nil], ["created_at", "2022-02-13 13:57:02.526484"], ["updated_at", "2022-02-13 13:57:02.526484"]]
TRANSACTION (8.1ms) commit transaction
TRANSACTION (0.1ms) begin transaction
EventMember Create (27.6ms) INSERT INTO "event_members" ("event_id", "member_id", "presented", "created_at", "updated_at") VALUES (?, ?, ?, ?, ?) [["event_id", 1], ["member_id", 2], ["presented", nil], ["created_at", "2022-02-13 13:57:02.561104"], ["updated_at", "2022-02-13 13:57:02.561104"]]
TRANSACTION (8.0ms) commit transaction
=>
[#<Member:0x00007efedf8ebfb8
id: 1,
name: "山田 太郎",
email: "yamada.taro@example.com",
created_at: Sun, 13 Feb 2022 13:36:46.345436000 UTC +00:00,
updated_at: Sun, 13 Feb 2022 13:36:46.345436000 UTC +00:00>,
#<Member:0x00007efedf8ebec8
id: 2,
name: "鈴木 花子",
email: "suzuki.hanako@example.com",
created_at: Sun, 13 Feb 2022 13:55:30.446672000 UTC +00:00,
updated_at: Sun, 13 Feb 2022 13:55:30.446672000 UTC +00:00>]
添加GraphQL查询
field :members, [Types::MemberType], null: false
field :events, [Types::EventType], null: false
我会在浏览器中进行操作确认。
{
members {
id
name
email
events {
id
title
startAt
}
}
}

{
events {
id
title
startAt
members {
id
name
email
}
}
}

可以获取到按照成员进行分类的活动,以及按照活动进行分类的成员。
突变的修正
我会修改Mutation,以便在事件注册时指定参与者。
module Mutations
class CreateEvent < BaseMutation
field :event, Types::EventType, null: false
argument :title, String, required: true
argument :start_at, GraphQL::Types::ISO8601DateTime, required: true
argument :end_at, GraphQL::Types::ISO8601DateTime, required: false
argument :place, String, required: false
argument :tags, String, required: false
argument :memo, String, required: false
argument :member_ids, [Integer], required: false
def resolve(member_ids:, **args)
event = Event.create!(**args).tap do|event|
member_ids.each{|member_id| event.event_members.create! member_id: }
end
{ event: }
end
end
end
※ 参数:member_ids…的行添加以及修正 def resolve。
※ 参数:正在进行 member_ids… 行的新增以及对 def resolve 的修正。
※ 参数:正在对 member_ids… 行进行添加并修正 def resolve。
确认操作
mutation {
createEvent(
input: {title: "懇親会", startAt: "2022-02-23T19:00:00Z", memberIds: [1, 2]}
) {
event {
id
title
members {
id
name
email
}
}
}
}

做得很棒
在Query中添加筛选条件
只需要一个选项,请将以下内容以中文母语进行改写:修复query_type.rb以便在事件和成员的查询中可以指定id。
module Types
class QueryType < Types::BaseObject
# Add `node(id: ID!) and `nodes(ids: [ID!]!)`
include GraphQL::Types::Relay::HasNodeField
include GraphQL::Types::Relay::HasNodesField
# 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"
def test_field
"Hello World!"
end
field :events, [Types::EventType], null: false do
argument :ids, [Integer], required: false
end
def events(ids: nil)
if ids
Event.where(id: ids)
else
Event.all
end
end
field :members, [Types::MemberType], null: false do
argument :ids, [Integer], required: false
end
def members(ids: nil)
if ids
Member.where(id: ids)
else
Member.all
end
end
end
end
在”field: events”和”field: members”上添加一个带有块的参数,如果在方法内指定了参数,则指定提取条件。
参数可以省略,设置为”ids: nil”。
最后
对于我的使用感受来说,与创建 REST API 相比,这种方法的代码量较少,而且只有一个端点,不会分散。我觉得这是一个很好的地方。