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

# 目的

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

```dockerfile:/dev/rails_graphsql/Dockerfile
FROM ruby
```

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

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

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

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

```shell:terminal
bundle install
```

## ruby と rails のバージョン確認

```shell:terminal
ruby -v
```

```shell:実行結果
ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [x86_64-linux]
```

```shell:terminal
rails -v
```

```shell:実行結果
Rails 7.0.2.2
```

## rails プロジェクトを作成

```shell:terminal
rails new --api --minimal .
```

## 動作確認

サーバ起動

```shell:terminal
rails s
```

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/720b74f7-d1ef-b012-7724-d4c68ee87edc.png)


## GraphQL 用の gem を追加

```diff_shell:terminal
bundle add graphql
- bundle add --group development graphiql sass-rails
+ bundle add --group development graphiql-rails sass-rails
```

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

## graphql 関連ファイルの生成

```shell:terminal
rails g graphql:install
```

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

### セッション有効化

application.rb に2行追加

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

### route に設定を追加

```ruby:routes.rb
  if Rails.env.development?
    mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql"
  end
```

## 動作確認

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/5f4d44af-9f8d-b771-03e2-790ca2b4337f.png)

テスト用に生成されているクエリを実行

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/aa02c8f7-d584-6978-db6d-91796ef27fb8.png)


# GraphQL のクエリ作成

今回はイベント予定表のAPIを想定して作ってみます

まずイベントの予定を管理するモデルと REST API を生成します


```shell: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
```

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

```shell: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
```

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

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

```ruby: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
```

```ruby: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
```

```ruby: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 を実行します

```shell: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 用の型を生成

```shell:terminal
rails g graphql:object Event
```

```:実行結果
      create  app/graphql/types/event_type.rb
```

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

```ruby: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 の分も生成します。

```shell:terminal
rails g graphql:object Member
```

```:実行結果
      create  app/graphql/types/member_type.rb
```

```shell:terminal
rails g graphql:object EventMember
```

```:実行結果
      create  app/graphql/types/event_member_type.rb
```

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

```ruby: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/5f4d44af-9f8d-b771-03e2-790ca2b4337f.png)

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

```graphql:クエリ
{
  events {
    id
    title
    startAt
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/71b13324-cc56-8897-ae9d-57768144aac4.png)

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

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

```shell:terminal
rails c
```

```ruby: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/9f578c8c-00ce-185d-3ebb-1e00a7ad87da.png)

投入したデータが返ってきますね

# 登録用の Mutation を作成

## Mutation　用のファイルを生成

```shell:terminal
rails g graphql:mutation CreateEvent
```

```:実行結果
      create  app/graphql/mutations/create_event.rb
```

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

```ruby:/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
```

これを修正します

```ruby:/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 を実行します

```graphql: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/482e9ab9-ee49-d900-a1cf-ed9b1f0d02c4.png)

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

```graphql:query
{
  events {
    id
    title
    startAt
    endAt
    place
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/dc075441-9d23-fbe0-8eac-77a0f5d61206.png)

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

## Member 用の Query と Mutation を追加

以下の Query を追加します

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

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

```graphql:query
{
    members {
        id
        name
        email
    }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/5d02e5fd-36f6-aa07-9a50-78e5fd7a276f.png)

Mutation を追加します

```shell:terminal
rails g graphql:mutation CreateMember
```

```:実行結果
      create  app/graphql/mutations/create_member.rb
```

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

```ruby: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 の確認をします

```graphql:mutation
mutation {
  createMember(
    input:{
      name: "山田　太郎"
      email: "yamada.taro@example.com"
    }
  ){
    member {
      id
      name
      email
    }
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/05d32c0f-a165-929b-9164-2253bf19ef68.png)

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

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

```ruby:/app/models/event.rb
class Event < ApplicationRecord
  has_many :event_members
  has_many :members, through: :event_members
end
```

```ruby:/app/models/member.rb
class Member < ApplicationRecord
  has_many :event_members
  has_many :events, through: :event_members
end
```

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

```shell:terminal
rails c
```

```ruby:rails_console
Member.create! name: '鈴木 花子', email: 'suzuki.hanako@example.com'
```

```ruby: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のクエリを追加します

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


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

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

```graphql:query
{
  members {
    id
    name
    email
    events {
      id
      title
      startAt
    }
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/cf137de5-014c-da60-4b2a-6cd2bedf0064.png)

```graphql:query
{
  events {
    id
    title
    startAt
    members {
      id
      name
      email
    }
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/8c0d219b-b278-9e1e-d398-784d3abcd08e.png)

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

Mutation の修正

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

```ruby:/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 を修正しています


動作確認

```graphql:mutation
mutation {
  createEvent(
    input: {title: "懇親会", startAt: "2022-02-23T19:00:00Z", memberIds: [1, 2]}
  ) {
    event {
      id
      title
      members {
        id
        name
        email
      }
    }
  }
}
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/82535/797b33b7-845e-fd12-f104-2d8e2e5f2759.png)

うまくできました

# Queryに抽出条件を追加

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

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