目的
Rails で GraphQL を使うときはどんな感じに書くのかを確認する
必要なもの
- 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 環境の Dokerfile を作成する
FROM ruby
VSCode のコマンド Reopen in Container で From Dockerfile から仮想環境に切り替える
rails インストール用の Gemfile を作成する
source "https://rubygems.org"
gem 'rails'
VSCode の terminal を開き 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 に2行追加
config.middleware.use ActionDispatch::Cookies
config.middleware.use ActionDispatch::Session::CookieStore
route に設定を追加
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
not null や 既定値を追加設定します
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
migration を実行します
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
Event 全件取得の GraphQL クエリを追加
field :events, [Types::EventType], null: false
def events
Event.all
end
graphiql で動作確認
rails s でサーバを起動して、 http://localhost:3000/graphiql にアクセスします
event の id, title, startAt(start_atだとエラーになる) を取得するクエリを実行
{
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 を作成
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
これを修正します
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 を実行します
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
}
}
正常に登録できていますね
Member 用の Query と Mutation を追加
以下の Query を追加します
field :members, [Types::MemberType], null: false
def members
Member.all
end
ブラウザで Query の確認をします
{
members {
id
name
email
}
}
Mutation を追加します
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 の確認をします
mutation {
createMember(
input:{
name: "山田 太郎"
email: "yamada.taro@example.com"
}
){
member {
id
name
email
}
}
}
イベント参加者の Query と Mutation を作成します
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
}
}
}
member ごとの events や event ごとの members が取得できていますね
Mutation の修正
event 登録時に参加者も指定できるよう 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
※ argument :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に抽出条件を追加
events, members の Query に id 指定できるように query_type.rb を修正します
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, と fields :members にブロックをつけて受け入れる引数を追加し、メソッド内で引数が指定されている場合は抽出条件を指定するようにしました
引数は ids: nil とすれば省略可能になります
さいごに
使った感想としては REST API を作るよりコード量は少なくてすみそうなのと、エンドポイントは1つだけなので散らからないところが良いと思いました。