4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Rails7のapiモードでGraphQLを使ってみた

Last updated at Posted at 2022-02-13

目的

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 を作成する

/dev/rails_graphsql/Dockerfile
FROM ruby

VSCode のコマンド Reopen in Container で From Dockerfile から仮想環境に切り替える

rails インストール用の Gemfile を作成する

/Gemfile
source "https://rubygems.org"
gem 'rails'

VSCode の terminal を開き gem をインストールする

terminal
bundle install

ruby と rails のバージョン確認

terminal
ruby -v
実行結果
ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [x86_64-linux]
terminal
rails -v
実行結果
Rails 7.0.2.2

rails プロジェクトを作成

terminal
rails new --api --minimal .

動作確認

サーバ起動

terminal
rails s

http://localhost:3000 にアクセスすれば rails が動作しているのが確認できます

image.png

GraphQL 用の gem を追加

terminal
bundle add graphql
- bundle add --group development graphiql sass-rails
+ bundle add --group development graphiql-rails sass-rails

※ graphiql は開発用ツール, sass-rails は graphiql 動作に必要

graphql 関連ファイルの生成

terminal
rails g graphql:install

graphiql が動作するよう設定を調整

セッション有効化

application.rb に2行追加

config/application.rb
  config.middleware.use ActionDispatch::Cookies
  config.middleware.use ActionDispatch::Session::CookieStore

route に設定を追加

routes.rb
  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 を生成します

terminal
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

次に参加者分を生成します

terminal
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

最後にイベントの参加者分を生成します

terminal
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 や 既定値を追加設定します

db/migrate/20220213115441_create_events.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
db/migrate/20220213115747_create_members.rb
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
db/migrate/20220213115822_create_event_members.rb
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 を実行します

terminal
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 用の型を生成

terminal
rails g graphql:object Event
実行結果
      create  app/graphql/types/event_type.rb

以下のように events テーブルから情報を取得して自動生成できていますね

app/graphql/types/event_type.rb
# 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 の分も生成します。

terminal
rails g graphql:object Member
実行結果
      create  app/graphql/types/member_type.rb
terminal
rails g graphql:object EventMember
実行結果
      create  app/graphql/types/event_member_type.rb

Event 全件取得の GraphQL クエリを追加

app/graphql/types/query_type.rbへ以下を追加
    field :events, [Types::EventType], null: false
    def events
      Event.all
    end

graphiql で動作確認

rails s でサーバを起動して、 http://localhost:3000/graphiql にアクセスします

image.png

event の id, title, startAt(start_atだとエラーになる) を取得するクエリを実行

クエリ
{
  events {
    id
    title
    startAt
  }
}

image.png

まだデータが一件もないので空の配列になっています

テストデータを rails c で投入

terminal
rails c
rails_console
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 を作成

Mutation 用のファイルを生成

terminal
rails g graphql:mutation CreateEvent
実行結果
      create  app/graphql/mutations/create_event.rb

以下のファイルが生成されました

/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

これを修正します

/app/graphql/mutations/create_event.rb修正後
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
mutation {
  createEvent(
    input:{
      title: "合宿"
      startAt: "2022-02-26T09:00:00Z"
      endAt: "2022-02-27T18:00:00Z"
      place: "有馬温泉"
      tags: "Ruby 合宿 温泉"
    }
  ){
    event {
      id
      title
    }
  }
}

image.png

クエリで登録内容を確認してみます

query
{
  events {
    id
    title
    startAt
    endAt
    place
  }
}

image.png

正常に登録できていますね

Member 用の Query と Mutation を追加

以下の Query を追加します

/app/graphql/types/query_type.rbへ追加
    field :members, [Types::MemberType], null: false
    def members
      Member.all
    end

ブラウザで Query の確認をします

query
{
    members {
        id
        name
        email
    }
}

image.png

Mutation を追加します

terminal
rails g graphql:mutation CreateMember
実行結果
      create  app/graphql/mutations/create_member.rb

Mutation ファイルを修正します。

app/graphql/mutations/create_member.rb修正後
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
mutation {
  createMember(
    input:{
      name: "山田 太郎"
      email: "yamada.taro@example.com"
    }
  ){
    member {
      id
      name
      email
    }
  }
}

image.png

イベント参加者の Query と Mutation を作成します

Event と Member は 多対多の関係にあるので先に Model クラスに関連を設定しておきます。

/app/models/event.rb
class Event < ApplicationRecord
  has_many :event_members
  has_many :members, through: :event_members
end
/app/models/member.rb
class Member < ApplicationRecord
  has_many :event_members
  has_many :events, through: :event_members
end

先に event_members にデータを投入しておきます

terminal
rails c
rails_console
Member.create! name: '鈴木 花子', email: 'suzuki.hanako@example.com'
rails_console
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のクエリを追加します

/app/graphql/types/event_type.rbへ追加
    field :members, [Types::MemberType], null: false
/app/graphql/types/member_type.rbへ追加
    field :events, [Types::EventType], null: false

ブラウザで動作確認します

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

image.png

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

image.png

member ごとの events や event ごとの members が取得できていますね

Mutation の修正

event 登録時に参加者も指定できるよう Mutation を修正します

/app/graphql/mutations/create_event.rb
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
mutation {
  createEvent(
    input: {title: "懇親会", startAt: "2022-02-23T19:00:00Z", memberIds: [1, 2]}
  ) {
    event {
      id
      title
      members {
        id
        name
        email
      }
    }
  }
}

image.png

うまくできました

Queryに抽出条件を追加

events, members の Query に id 指定できるように query_type.rb を修正します

/app/graphql/types/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つだけなので散らからないところが良いと思いました。

4
5
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?