[2018年最新版]RubyonRailsを使ってGraphQLを学んでみる

## はじめに

この記事は、現在実務でGraphQLを使っている筆者が、Inputで得た知見を自分なりにまとめたものです。

公開してはいますが、日々キャッチアップしたら都度更新していくので、優しく見守っていただけると！

記載している文章内で不備などありましたら、ご指摘いただけると幸いです。

## 対象となる読者
* Class_BaseにGraphQLの書き方を知りたい人
* GraphQLでのMutationの書き方がいまいちわからない人
* とりあえず、RailsとGraphQLでCRUDをやってみたい人

## 前提

GraphQLってなに？などの概念的な話はあまり出てきません。
それらについては、他のサイトや記事でたくさん紹介されているので、参考リンクを参照ください。

### 参考リンク

https://qiita.com/syu_chan_1005/items/3350f1d12c17a77e98c7

https://qiita.com/bananaumai/items/3eb77a67102f53e8a1ad

https://www.webprofessional.jp/rest-2-0-graphql/

## GraphQlとRESTの違い

|GraphQl|REST|
|:--|:--|
|HTTP上などで動作する|HTTP上などで動作する|
|エンドポイントが一つだけ|エンドポイントが複数ある|
|1リクエストで多くのリソースを包括|1リクエストにつき1リソース|
|フェッチしない|フェッチが過剰|

## Gemfileにgraphqlを追加

```ruby
gem 'graphql'
```

#### GraphQLをGenerateする

```bash
rails g graphql:install
```

次のようにファイル群が作成される。

```bash

Running via Spring preloader in process 75061
      create  app/graphql/types
      create  app/graphql/types/.keep
      create  app/graphql/bookshelf_schema.rb
      create  app/graphql/types/base_object.rb
      create  app/graphql/types/base_enum.rb
      create  app/graphql/types/base_input_object.rb
      create  app/graphql/types/base_interface.rb
      create  app/graphql/types/base_union.rb
      create  app/graphql/types/query_type.rb
add_root_type  query
      create  app/graphql/mutations
      create  app/graphql/mutations/.keep
      create  app/graphql/types/mutation_type.rb
add_root_type  mutation
      create  app/controllers/graphql_controller.rb
       route  post "/graphql", to: "graphql#execute"
Skipped graphiql, as this rails project is API only
  You may wish to use GraphiQL.app for development: https://github.com/skevy/graphiql-app
```

## GraphiQLをbrewでinstall

公式ページ: https://electronjs.org/apps/graphiql

brewを使って、MacにGraphiQLをインストールする

```bash
brew cask install graphiql
```

## GraphiQLでクエリを投げてみる

GraphQL Endpointに以下のURLを入力する

`http://api.graphloc.com/graphql`

次に、緯度と経度のクエリを投げて、動作確認してみる

```bash
{
  getLocation(ip: "8.8.8.8") {
    location {
      latitude
      longitude
    }
  }
}
```

成功すると、次のようなデータが返ってくる。

```bash
{
  "data": {
    "getLocation": {
      "location": {
        "latitude": "37.751",
        "longitude": "-97.822"
      }
    }
  }
}
```

## GrahiQLの操作

`Rails server`でアプリケーションサーバーを起動させていれば、GraphiQLでも動作させることができる。

![スクリーンショット 2018-09-15 23.20.27.png](https://qiita-image-store.s3.amazonaws.com/0/195539/b4d6b19b-86cc-e998-84de-dc005b3cc640.png)

正しく起動できていれば、`< Docs`が開けるようになるので、クリックすると`Documentation Explorer`が開く。

![スクリーンショット 2018-09-15 23.23.38.png](https://qiita-image-store.s3.amazonaws.com/0/195539/81f60b6d-48c9-3e3b-ae5e-86ad8b69eaaa.png)

`Query`をクリックすると、現在プロジェクト内の`query_type.rb`で定義されているクエリが確認できる。

JSONについては、以下のWikipediaを参照のこと。

https://en.wikipedia.org/wiki/JSON

## query_typeの書き方

`rails g graphql:install`を実行すると、以下のような`query_type.rb`が生成される

```ruby
module Types
  class QueryType < Types::BaseObject
    # 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　XYZ"
    def test_field
      "Hello World!"
    end
  end
```

表示されている項目を説明すると、

|field|:test_field|String|null: false|description
|:--|:--|:--|:--|:--
|fieldを定義|DBでいうところのカラム名|fieldのデータ型|nullを許可するかどうか|fieldの説明

こんな感じ。

### filedに引数を定義したい場合

書き方としては、ブロックを作り、そこに`argument`として引数を定義。

定義する際には、データ型と必須であれば`required: true`を記載する。

あとは、メソッドの引数に通常通り与えてあげればOK。

```ruby
module Types
  class QueryType < Types::BaseObject
    field :test_field, String, null: false,
      description: "An example field added by the generator　XYZ"do
      argument :name, String, required: true
    end
    # フロント側で呼び出すfieldをメソッド形式を定義
    def test_field(name:)
      "Hello #{name}"
    end
  end
end
```

GraphiQLをリロードして確認してみると、引数に`name`が追加されているのがわかる。

![スクリーンショット 2018-09-15 23.54.43.png](https://qiita-image-store.s3.amazonaws.com/0/195539/1f76d5b9-89eb-9748-50c0-4b31d5d9416a.png)

testFieldの引数に`name`を指定して、適当な文字列を追加してあげると、ちゃんと定義通りに反映されているのがわかる。

![スクリーンショット 2018-09-15 23.56.20.png](https://qiita-image-store.s3.amazonaws.com/0/195539/7400ca36-4cba-82a8-4577-1c22666b745b.png)

## Contextについて

contextは各クエリ固有の情報を、後述するresolveメソッドにhash形式で渡す役割を持っている。

`graphql_controller.rb`を開くと、次のようなコードになっているのがわかります。

```ruby
class GraphqlController < ApplicationController
  def execute
    variables = ensure_hash(params[:variables])
    query = params[:query]
    operation_name = params[:operationName]
    context = {
        time: Time.now
      # Query context goes here, for example:
      # current_user: current_user,
    }
    result = BookshelfSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
    render json: result
  rescue => e
    raise e unless Rails.env.development?
    handle_error_in_development e
  end

  private

  # Handle form data, JSON body, or a blank value
  def ensure_hash(ambiguous_param)
    case ambiguous_param
    when String
      if ambiguous_param.present?
        ensure_hash(JSON.parse(ambiguous_param))
      else
        {}
      end
    when Hash, ActionController::Parameters
      ambiguous_param
    when nil
      {}
    else
      raise ArgumentError, "Unexpected parameter: #{ambiguous_param}"
    end
  end

  def handle_error_in_development(e)
    logger.error e.message
    logger.error e.backtrace.join("\n")

    render json: { error: { message: e.message, backtrace: e.backtrace }, data: {} }, status: 500
  end
end

```

この中に`context = {}`という箇所があり、そこにresolveメソッドに渡したい`context`を定義します。

```ruby
(省略)
context = {
        time: Time.now
    }
(省略)
```

続いて、`query_type.rb`でcontext[:time]`を渡してあげましょう。

```ruby
(省略)
def test_field(name:)
  Rails.logger.info context[:time]
  "Hello #{name}!"
end
(省略)
```

この状態でGraphiQLでクエリを投げ、Railsのログを見てみると、

```ruby
Started POST "/graphql" for 127.0.0.1 at 2018-09-16 00:13:18 +0900
Processing by GraphqlController#execute as HTML
  Parameters: {"query"=>"{\n  testField(name: \"Ruby\")\n}", "variables"=>nil, "graphql"=>{"query"=>"{\n  testField(name: \"Ruby\")\n}", "variables"=>nil}}
2018-09-16 00:13:18 +0900 #=> context = { time: Time.now }で定義した部分
Completed 200 OK in 43ms (Views: 0.7ms | ActiveRecord: 0.0ms)
```

現在の時刻が表示されているのがわかります。

使い所としては、例えば、現在ログインしているのが誰なのかを表す`current_user`などを渡せば、ログで確認することができるということですね。

## Scalarについて

スカラーは、GraphQLで定義したオブジェクトごとのキーと値を独自のデータ型で管理したもの。

GraphQLでデフォルトで用意されているスカラー型は、以下の5つ。

|String|Int|Boolean|Id|Float|
|:--:|:--:|:--:|:--:|:--:|
|UTF-8のシーケンス|符号付き32ビットの整数|true or false|オブジェクトごとに割り振られたID|符号付き倍精度浮動小数点値|

ただ、スカラー型については独自に定義することができるので、これを応用すればカスタマイズすることが可能。

## Null制約について

GraphQLでは、それぞれのfieldごとにnull制約をつけることができる。

`null; false`を設定したfieldは、フロント側でクエリを取得した時やmutationを実行した際にエラーが返るようになる。

![スクリーンショット 2018-09-16 11.23.29.png](https://qiita-image-store.s3.amazonaws.com/0/195539/72bae4f9-f99a-e8b5-642d-4712ff62da49.png)

サーバーサイド側でもnull制約を設けたい場合は、別途バリデーション定義をすればOK。

## 独自にQuery_typeを定義する

ActiveRecordを使って、独自のQuery_typewを定義することが可能。

例えば、`full_name`というメソッドを独自に定義したい場合は、該当するモデルに定義し、query_typeにそのfieldを追記してあげればいい。

```ruby
# author.rb
class Author < ApplicationRecord
  def full_name
    ([first_name, last_name].compact).join " "
  end
end
```

```ruby
# author_type.rb
class Types::AuthorType < Types::BaseObject
  (省略)
  field :full_name, String, null: false, camelize: false
end
```

ちなみに、モデルにロジックを書かなくても、各`query_type.rb`に記載しても同じ結果が得られる。

その場合は、定義するobjectのfield名のprefixに`object`をつけてあげればOK。
(GraphQL側でObjectの対象となるモデル名を検知してくれる)

```ruby
# author_type.rb
class Types::AuthorType < Types::BaseObject
  (省略)
  field :full_name, String, null: false, camelize: false

  def full_name
    ([object.first_name, object.last_name]).compact).join " "
  end
end
```

GraphiQLで実際に叩いてみると、定義されたfieldが正常にresponseされるのがわかる。

![スクリーンショット 2018-09-16 12.02.19.png](https://qiita-image-store.s3.amazonaws.com/0/195539/96bd3673-6139-dbd6-7165-f3c3a7712049.png)

## fieldTypeをカスタマイズする

GraphQLでは、Fieldtypeも独自定義することが可能。

やり方は簡単で、

1. モデルに独自定義したいfieldTypeをメソッドとして定義する
1. `graphql/types`ディレクトリ配下に新規で1と同じ名前の`query_type.rb`を作成
1. 取得するためのfieldを定義する
1. モデルに対応した各query_type.rbにfieldを追記する


#### モデルに独自定義したいfieldTypeをメソッドとして定義する

例えば、緯度と経度を作成する`coordinates`というFieldTypeをメソッドとしてモデルファイルに定義する。

```ruby
class Author < ApplicationRecord

  def coordinates
    # 緯度と経度について、それぞれ定義するメソッド
    # 第一要素が緯度で第二要素が経度
    [rand(90), rand(90)]
  end
end
```

#### `graphql/types`ディレクトリ配下に新規で1と同じ名前の`query_type.rb`を作成

```bash
touch app/graphql/types/coordinates_type.rb
```

#### ルートレベルで取得するためのfieldを定義する

```ruby
# coordinates_type.rb
class Types::CoordinatesType < Types::BaseObject
  field :latitude, Float, null: false
  field :longitude, Float, null: false

  def latitude
    object.first
  end

  def longitude
    object.last
  end
end
```

####  モデルに対応した各query_type.rbにfieldを追記する

```ruby
# author_type.rb
class Types::AuthorType < Types::BaseObject
  field :coordinates, Types::CoordinatesType, null: false
end
```
## QueryTypeに配列を定義する

配列を定義するにはまず、データベースの玄関口であるモデルに対象となるfieldをメソッドで定義する。

```ruby
# author.rb
class Author < ApplicationRecord
  def publication_years
    # 出版した年度が配列でランダムに最大10個返ってくる
    (1..rand(10)).to_a.map { 1900 - rand(100)}
  end
end
```

次に、各query_typeを定義しているファイルに、fieldを定義する。

```ruby
class Types::AuthorType < Types::BaseObject
  field :publication_years, [Int], null: true
end
```

最後に、ルートレベルのquery_type.rbにfieldとArrayに対応した呼び出しメソッドを定義する。

```ruby
field :authors, [Types::AuthorType], null: false

def authors
  Author.all
end
```

これでGraphiQLを叩くと、複数形にすればArrayでQueryが取得できる。

![スクリーンショット 2018-09-16 14.21.00.png](https://qiita-image-store.s3.amazonaws.com/0/195539/2809b839-0cbd-f6b3-7d6f-010cef0d9e7b.png)

## MutaionによるCRUD処理

Mutationはデータの変更を行うときに使うfield。

Create,Update, Deleteのような既存のfieldを変更する際に使用される。

### MutationによるCreate

MutationでCreateを行う方法は2通りある。

#### その1:typesディレクトリ内にあるmutation_type.rbに記述する

```ruby
# app/graphql/types/mutation_type.rb
module Types
  class MutationType < Types::BaseObject

    field :create_author, AuthorType, null: true, description: "Create an author" do
      argument :first_name, String, required: false, camelize: false
      argument :last_name, String, required: false, camelize: false
      argument :yob, Int, required: false
      argument :is_alive, Boolean, required: false, camelize: false
    end

    def create_author(first_name:, last_name:, yob:, is_alive:)
      Author.create first_name: first_name, last_name: last_name, yob: yob, is_alive: is_alive
    end

  end
end
```

create処理をするfieldを用意して、引数に何を与えるかの処理をブロックで記述。

```ruby
 field :create_author, AuthorType, null: true, description: "Create an author" do
   argument :first_name, String, required: false, camelize: false
   argument :last_name, String, required: false, camelize: false
   argument :yob, Int, required: false
   argument :is_alive, Boolean, required: false, camelize: false
 end
```

その後、ActiveRecordによるデータベースに問い合わせるための処理をメソッドの形式で定義する。


```ruby
 def create_author(first_name:, last_name:, yob:, is_alive:)
      Author.create first_name: first_name, last_name: last_name, yob: yob, is_alive: is_alive
 end
```

#### その2:mutationsディレクトリにmutation.rbを作成して記述する

```ruby
class Mutations::CreateAuthor < GraphQL::Schema::Mutation
  null true

  argument :first_name, String, required: false, camelize: false
  argument :last_name, String, required: false, camelize: false
  argument :yob, Int, required: false
  argument :is_alive, Boolean, required: false, camelize: false

  def resolve(first_name:, last_name:, yob:, is_alive:)
    Author.create first_name: first_name, last_name: last_name, yob: yob, is_alive: is_alive
  end
end
```

mutationsディレクトリにmutation.rbを作成し、createする際に必要な引数を定義する

```ruby
  argument :first_name, String, required: false, camelize: false
  argument :last_name, String, required: false, camelize: false
  argument :yob, Int, required: false
  argument :is_alive, Boolean, required: false, camelize: false
```

その後、resolveメソッドを定義し、引数に必要なもfieldを渡し、ActiveRecordでDBへ問い合わせるための処理を記述する。

```ruby
  def resolve(first_name:, last_name:, yob:, is_alive:)
    Author.create first_name: first_name, last_name: last_name, yob: yob, is_alive: is_alive
  end
```

最後に、typesディレクトリ内にあるmutation_type.rbで定義したmutationを呼び出してあげれば、その1と挙動が同じになる。

```ruby
module Types
  class MutationType < Types::BaseObject
    field :create_author, Types::AuthorType, mutation: Mutations::CreateAuthor
  end
end
```

### Query_Variableについて

![スクリーンショット 2018-09-16 16.45.56.png](https://qiita-image-store.s3.amazonaws.com/0/195539/87a7b5e1-f8fb-96e6-8e40-1004610416e3.png)


GraphiQLでMutationを実行するとき、`query_variable`を使うと、fieldの値を別ウィンドウで定義できるので便利。

```graphql
mutation createAuthor($first_name:String, $last_name:String, $yob:Int, $is_alive:Boolean){
  createAuthor(first_name:$first_name, last_name:$last_name, yob:$yob, is_alive:$is_alive) {
    id
    full_name
  }
}
```

`mutation`に続いてMutation名を記述し、field名のprefixに`$`をつけてあげる。

```graphql

createAuthor($first_name:String, $last_name:String, $yob:Int, $is_alive:Boolean)
```

続いて、mutationの箇所には`$`のついたfield名を渡してあげる。

```graphql
createAuthor(first_name:$first_name, last_name:$last_name, yob:$yob, is_alive:$is_alive)
```

あとは、GraphiQLの`QUERY VARIABLES`の箇所にJSON形式でfield名に対応した渡したい値を記述してあげればOK。

```json
{
  "first_name": "Yammy",
  "last_name": "Humberg",
  "yob": 2019,
  "is_alive": true
}
```

成功すると、次のようなresponseになる。

```json

{
  "data": {
    "createAuthor": {
      "id": "7",
      "full_name": "Yammy Humberg"
    }
  }
}
```

## InputTypesについて

これまでCreateを行うときには、GraphiQLで次のように書いてきました。

```graphql
mutation createAuthor($first_name:String, $last_name:String, $yob:Int, $is_alive:Boolean){
  createAuthor(first_name:$first_name, last_name:$last_name, yob:$yob, is_alive:$is_alive) {
    id
    full_name
  }
}
```

ただ、これだと同じfield名が2回も登場していて、全然DRYではないですよね。

そこで登場するのが、InputTypeです。

InpuTypeは、`GraphQL::Schema::InputObject`を継承させて次のように定義します。

```ruby
class Types::AuthorInputType < GraphQL::Schema::InputObject
  graphql_name "AuthorInputType"
  description "All the attributes for creating an author"

  argument :id, ID, required: false
  argument :first_name, String, required: false, camelize: false
  argument :last_name, String, required: false, camelize: false
  argument :yob, Int, required: false
  argument :is_alive, Boolean, required: false, camelize: false
end
```

InputTypeの各項目については、次の通りです。

|argument|required|graphqql_name|description|
|:--:|:--:|:--:|:--:|
|InputTypeに必要な引数|必須かどうか|InputType名|InputTypeの説明|

次に、MutationTypeで定義したCreateをInputTypeで書き換えます。

```ruby
class Types::MutationType < Types::BaseObject
   field :create_author, AuthorType, null: true, description: "Create an author" do
    argument :author, Types::AuthorInputType, required: true
  end
  def create_author(author:)
    Author.create author.to_h
  end
 end
```

あとはGraphiQLで以下のようにクエリを投げ、`QUERY VARIABLES`で値を指定すればOK。

![スクリーンショット 2018-09-17 12.49.15.png](https://qiita-image-store.s3.amazonaws.com/0/195539/c99ed487-570e-5ff9-b042-e05e02db4e9d.png)

#### QUERY

```graphql

mutation createAuthor($author:AuthorInputType!) {
  createAuthor(author:$author) {
    id
    full_name
  }
}
```

#### QUERY VARIABLES

```json
{ "author": {
  "first_name": "Hina",
  "last_name": "Okamoto",
  "yob": 1933,
  "is_alive": true
  }
}
```

### MutationによるUpdate

![スクリーンショット 2018-09-18 09.21.58.png](https://qiita-image-store.s3.amazonaws.com/0/195539/d584049a-c891-46c7-f877-fb5c928c0739.png)


InputTypeまでできたら、あとは簡単。個別にUpdateのMutationを作成してあげればいいんです。

```ruby
class Types::MutationType < Types::BaseObject
  field :update_author, Boolean, null: false, description: "Update an author" do
    argument :author, Types::AuthorInputType, required: true
  end

  def update_author(author:)
    existing = Author.where(id: author[:id]).first
    #&.でnil以外なら更新されたデータのhashを返す
    existing&.update author.to_h
  end

```

ここで、`update_author`に`Boolean`の指定があることに、疑問を持った方がいるかもしれません。

これは、フロント側はデーターベース側にどんな変更があったかを知る必要がないので、基本的にApplicationRecordの戻り値を渡さなくてもOKだから。

そのため、フロント側ではBoolean型にしてupdateがtrueなのかfalseなのかを返せばいいので、このようになっています。

### MutationによるDelete

![スクリーンショット 2018-09-18 09.28.06.png](https://qiita-image-store.s3.amazonaws.com/0/195539/2a2d1a21-bb5f-977e-809a-7a9ec9f5da89.png)


deleteではInputTypeを使わず、削除対象となるIDを渡してあげればOK。

```ruby
class Types::MutationType < Types::BaseObject
  field :delete_author, Boolean, null: false, description: "Delete an author"do
    argument :id, ID, required: true
  end

  def delete_author(id:)
    Author.where(id: id).destroy_all
    true
  end
end
```

destroy_allによって、ActiveRecordを介してDBにIDを問い合わせ、レコードを削除しています。