我尝试在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正在运行。

image.png

添加适用于 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。

image.png

执行为测试生成的查询

image.png

创建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。

image.png

执行查询以获取事件的id,标题和开始时间

{
  events {
    id
    title
    startAt
  }
}
image.png

由于还没有任何数据,所以它是一个空数组。

在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 查询。

image.png

你投入的数据会返回来的。

我要创建一个用于注册的突变。

生成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
    }
  }
}
image.png

我试着查询一下注册内容。

{
  events {
    id
    title
    startAt
    endAt
    place
  }
}
image.png

你已经成功注册了呢。

添加适用于会员的查询和变更操作

我将添加以下查询

    field :members, [Types::MemberType], null: false
    def members
      Member.all
    end

我会在浏览器中确认查询内容。

{
    members {
        id
        name
        email
    }
}
image.png

我們將添加突變。

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
    }
  }
}
image.png

创建参与活动的查询和变更。

创造活动参与者的查询和变更。

由于事件(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
    }
  }
}
image.png
{
  events {
    id
    title
    startAt
    members {
      id
      name
      email
    }
  }
}
image.png

可以获取到按照成员进行分类的活动,以及按照活动进行分类的成员。

突变的修正

我会修改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
      }
    }
  }
}
image.png

做得很棒

在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 相比,这种方法的代码量较少,而且只有一个端点,不会分散。我觉得这是一个很好的地方。

bannerAds