GraphQL Ruby の使い方 (基礎編)

# :warning: 注意
現在の　graphql-ruby　（ver: 1.8.x） は Ruby のクラスベースによる定義がメインで、この記事に書いてある DSL を使った定義は __非推奨__ です。
[ロードマップ](https://graphql-ruby.org/schema/class_based_api#roadmap) には DSL を使った `.define-style` は、graphql-ruby 2.0 で廃止するとあります。
新しいクラスベースでの書き方は [公式ドキュメント](http://graphql-ruby.org/getting_started) を御覧ください。
日本語の記事だと @gfx さんの [「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ](https://employment.en-japan.com/engineerhub/entry/2018/12/26/103000#GraphQL-API%E3%82%92%E5%AE%9F%E8%A3%85%E3%81%97%E3%81%A6%E3%81%BF%E3%82%8B) が非常によくまとまってて分かりやすいです。


# はじめに
私が携わっているプロダクトでは、フロントエンド用のAPIとしてGraphQLを採用しました。
実際に使ってみてかなり良い感じなのですが、最初はいざ実装しようにもよく分からずに苦労しました。
そこで、GraphQL の実装方法についてサーバーサイドに焦点を当てて書いていきたいと思います。
ちなみに基礎編とありますが、基礎的な内容なので基礎編です。応用編があるかは分かりません。(もしかしたら書くかも)

GraphQLはいいぞ。

# GraphQLとは何か
[公式の定義](http://graphql.org/learn/)によると「自分のAPIに対して使えるクエリ言語の一種」です。
他にも

- https://qiita.com/icoxfog417/items/92214aed64f47dfeade5
- http://blog.bitjourney.com/entry/2017/08/19/161308

などを読むと、大体どんなものか掴めると思います。

個人的に実装していて思うのは、GraphQLはRESTの制約をガチガチに固めた上でAPIとエンドポイントとの固い結合を解いたものだな〜ということです。
GraphQLではデータの取得や更新を基本的にPOSTで行います ([仕様ではGETも定められています](http://graphql.org/learn/serving-over-http/#get-request))。
このPOSTのリクエストパラメーターにクエリを生やすことで、APIに対して行いたい処理を指定します。

この時クライアント側は、クエリを好きな組み合わせで、欲しいデータや更新したいデータのみを指定して、APIを叩くことができます。そのため、たとえフロント側の構成やページ遷移が決まっていなくても、バックエンドはAPIの開発をし続けることができます。

このように、クライアントからは扱いたいデータを自由に選択でき、バックエンドは実装の負荷を下げられるのがGraphQLの良いところです。

# GraphQLの実装
GraphQL自体はあくまで仕様で、実装は様々なものがあります。以下に一例を挙げます。
([awesome-graphql](https://github.com/chentsulin/awesome-graphql)にGraphQLの実装の一覧が載っています。)

- [Graphql Ruby](https://github.com/rmosolgo/graphql-ruby)
- [GraphQL.js](https://github.com/graphql/graphql-js)
- [Graphcool](https://github.com/graphcool/framework)
- [AWS AppSync(Public Preview)](https://aws.amazon.com/jp/appsync/)

これらは大まかに２種類に分けることができます。Graphql RubyやGraphQL.jsは既存のサーバーサイドフレームワークと組み合わせて使うのですが、GraphcoolやAppSyncはそれそのもので完結していて比較的簡単にAPIサーバーを実装することができます。Graphcoolは最近オープンソースになったのでイケイケです。

今回はRailsと組み合わせて使うことができるGraphQL Rubyで話を進めていきます。

# GraphQLを書いてみる
GraphQLがどういったものか、というのは実際に書いてみるのが一番理解しやすい(説明しやすい)ので、ここからはサンプルアプリを作りながら解説していきます。
サンプルアプリはこちらです -> https://github.com/sukechannnn/graphql-ruby-demo/
`plain-rails`ブランチにGraphQLを実装していない状態を残しておきました。これを使って一緒にGraphQL APIを実装してみましょう！(事前に`bin/setup`を実行しておいてください。)

## Install
まずは`graphql`gemをインストールします。

```ruby
gem 'graphql', '1.7.7'
```

```console
$ bundle install
```

## Hello World!
Generatorがあるので、それを使ってgraphql-rubyの雛形を作りましょう。

```console
$ rails g graphql:install
```

すると、このようなディレクトリツリーが出力されます。中身は後で説明します。

```
app/graphql
├── graphql_ruby_demo_schema.rb
├── mutations
└── types
    ├── mutation_type.rb
    └── query_type.rb
```

また、`graphiql`というブラウザ上でGraphQL APIを動かして動作を確認できるツールがあります。それのRails版をdevelopment groupにインストールしましょう。

```ruby
  gem 'graphiql-rails'
```

```console
$ bundle install
```

ルーティングにgraphiqlのパスを追加します。

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

はい、ここまでで一度`rails s`して http://localhost:3000/graphiql にアクセスしてみてください。
graphiqlの画面が表示されていれば設定は完了です。試しにクエリを投げてみましょう。

```graphql
{
  testField
}
```

`Hello World!`が返ってくれば設定は完了です！

# QueryとMutationとSubscription
GraphQLで問い合わせをするための最も基本的な概念としてQueryとMutationとSubscriptionがあります。

### Query
データを取得するための仕組みです。

### Mutation
データを更新するための仕組みです。

### Subscription
Websocketを扱うための仕組みです。ここでは触れません。

## Query
ここからは実際にQueryを定義してデータを取得できるようにしていきます。まずはseed_fuで入れたデータ(`db/fixtures/sample_data.rb`)を取得できるようにしましょう。

### UserType

まずはUserモデルからです。graphql-rubyにはgeneratorがあるのでそれを使います。

```
$ rails g graphql:object User id:ID! name:String! email:String!
```

すると、以下のようなファイルが`app/graphql/types/user_type.rb`に生成されます。(見やすさのために少し整形しています)

```ruby
Types::UserType = GraphQL::ObjectType.define do
  name 'User'

  field :id, !types.ID
  field :name, !types.String
  field :email, !types.String
end
```

簡単に説明します。ここでは`Types::UserType`を`GraphQL::ObjectType`で定義しています。`ObjectType`は基本となるオブジェクト型で、これからもよく出てきます。
`name`はschemaの定義に用います。詳しくは後で説明します。
`field`はどのデータにアクセスするかを定義しています。graphql-rubyでは対応するモデルのattributeと同じ名前でfieldを定義すると、それだけでデータを取得することができます。

これで`UserType`を定義することはできたのですが、データを取得するためにはこれをQueryと紐付けないといけません。(逆に言うと定義した型は再利用が可能です。)
Queryと紐付けるためには最初にgenerateした`Types::QueryType`内にfieldを書いていきます。

```ruby
Types::QueryType = GraphQL::ObjectType.define do
  name 'Query'

  field :user, !Types::UserType do
    resolve ->(_obj, _args, ctx) {
      ctx[:current_user]
    }
  end
end
```

ここで、`resolve`というものが出てきました。これはGraphQL内でロジックを書くための仕組みです。graphql-rubyでは単純にfieldのみを書くと、そのattribute(またはメソッド)が生えていない場合にエラーで落ちます。また、attributeが生えている場合でもデータを加工して返したい場合もあると思います。そのような時にこのresolveを使うことで解決することができます。

resolveに渡すlambdaには `obj`, `args`, `ctx` という3つの引数があります。

- `obj` は自身のオブジェクトで、例えばUserTypeであればuserの情報が入っています
    - QueryTypeでは使いません
- `args` はfieldに渡す引数です
    - ここでは触れません、詳しくは[こちら](http://graphql.org/learn/queries/#arguments)
- `ctx` はログインユーザーの情報など重要な情報を渡すために使います

ということで`ctx`がとても重要です。ログイン機能が付いているアプリでは`current_user`を渡してあげるのが常套手段です。この`ctx`はgraphql-rubyがgenerateした`GraphqlController`で指定します。見てみるとコメントにそれらしいことが書いてあるのが分かると思います。今回はログイン機能がないので、`User.last`を`current_user`とすることとします。

```ruby
class GraphqlController < ApplicationController
  def execute
    variables = ensure_hash(params[:variables])
    query = params[:query]
    operation_name = params[:operationName]
    context = {
      current_user: User.last,  # ここでcurrent_userを指定する
    }
    result = GraphqlRubyDemoSchema.execute(query, variables: variables, context: context, operation_name: operation_name)
    render json: result
  end

...
```

はい、ここまで来ればGraphQLで値を取得することができます。graphiqlから以下のクエリを投げてみてください。seedで登録したデータが返ってくるはずです。

```graphql
{
  user {
    id
    name
    email
  }
}
```

graphql-rubyではこんな感じのDSLでAPIを定義していきます。最初は違和感があると思いますが、慣れるとフォーマットがある程度決まっているので簡単です。(最近[Classで定義できるPRがマージ](https://github.com/rmosolgo/graphql-ruby/pull/1037)されてバージョン1.8でリリースされる予定なので、DSLが気持ち悪いと感じた方はもう少しお待ち下さい。)

## AddressType
次に、Userと1対1で紐付いているAddressにアクセスできるようにします。まずは`AddressType`を作りましょう。

```
$ rails g graphql:object Address id:ID! postal_code:Int! address:String!
```
もしActiveRecordを使ってAddressを取得する場合、直接Addressを取りに行くのではなくUserから辿ると思います。それはGraphQLでも同じです。なので、Userにfieldを生やしてあげます。

```ruby
Types::UserType = GraphQL::ObjectType.define do
  name 'User'

  field :id, !types.ID
  field :name, !types.String
  field :email, !types.String
  field :address, !Types::AddressType  # 追加する
end
```

これでAddressを取得できるようになりました。graphiqlから以下のクエリを投げてみてください。

```graphql
{
  user {
    id
    name
    email
    address {
      postal_code
      address
    }
  }
}
```
住所と郵便番号が返ってきたでしょうか。これでQueryの基本的な部分の説明はだいたい終わりました。最後にConnectionについて軽く触れておきます。

## Connection
モデルにはUserがつぶやきを投稿できるような場合を想定して、PostがUserに対して1対多で生えています。これを取得できるようにしたいです。なにはともあれ`PostType`をgenerateしましょう。

```
$ rails g graphql:object Post id:ID! subject:String!
```
UserTypeに対してPostTypeが複数あるような場合、fieldではなく`connection`を使って関連を表します。

```ruby
Types::UserType = GraphQL::ObjectType.define do
  name 'User'

  field :id, !types.ID
  field :name, !types.String
  field :email, !types.String
  field :address, !Types::AddressType
  connection :posts, !Types::PostType.connection_type  # 追加する
end
```

これでPostsをまとめて取れるようになりました。graphiqlから以下のクエリを投げてみてください。

```graphql
{
  user {
    id
    name
    email
    posts {
      edges {
        node {
          id
          subject
        }
      }
    }
  }
}
```
Postの一覧が取得できたと思います。並び順を変えたい場合は、Resolverを使って`order_by`すれば良い感じになります。詳しくは[こちら](http://graphql-ruby.org/relay/connections.html)をご覧ください。

Connectionを使うとページネーションの仕組みも一緒に付いてくるので非常に便利です。Connectionのクライアント側からの詳しい使い方については下記を参照してください。
https://facebook.github.io/relay/graphql/connections.htm

# いくつか補足
ここまで駆け足で説明してきたので、いくつか説明が抜けている部分があります。それらをここで簡単に補足します。

## 型
GraphQLには型があります。

- `Int`: A signed 32‐bit integer
- `Float`: A signed double-precision floating-point value
- `String`: A UTF‐8 character sequence
- `Boolean`: true or false
- `ID`: ユニークな文字列で、CacheやConnectionなどRelay由来の仕組みに使われます

また、独自にScalarTypeを定義することもできます。

## Relayとは
[Relay](https://facebook.github.io/relay/)はFacebookが開発しているGraphQLを使ったFluxの実装です。ReactとGraphQLの性能を最大限引き出すことができるようで、GC等のかなり強力な機能が付いています。FluxなのでReduxと組み合わせて使うことはできません。Redux&ReactとGraphQLを組み合わせたい場合は[Apollo](https://www.apollographql.com/)を使うといいです。

Relayは独自にGraphQLを拡張しているのですが、いくつかの機能がGraphQLにも取り入れられています。特にConnectionType(ページネーションの仕組み)はGraphQL本体の仕様になったと言っても過言ではありません。

## 型に付いてる！マーク
`field :name, !types.String`の`!`マークはnullable falseを表します。`!`マークが付いていなければ、そのfieldはnullableです。基本的にnullable falseで定義していくのが良いと思います。

# Mutation
ここからはMutationを定義してデータを更新できるようにします。これから説明するのはRelay由来の方のMutationです。こちらの方がすっきり書けて見通しがよく、またコードの再利用ができるようになっています。元のMutationはこちらが理解できれば簡単に書けるので説明は割愛します。

## Mutations::UpdateAddressMutation
まずはAddressを変更できるようにしてみましょう。Mutationのファイルをgenerateします。

```
$ rails g graphql:mutation UpdateAddressMutation
```

すると、コメントに`input_field`と`return_field`と`resolve`を定義するように書いてあります。これらを定義してあげればMutationが作れそうです。

`input_field`はそのままで、Mutationに使うインプットです。今回は変更したい住所と郵便番号を設定します。

`return_field`は返り値です。アップデートした後の各fieldの値を返すために、Queryで使うために定義した`Types::AddressType`を使います。

そして、`resolve`でアップデートのロジックを書くことで実装は完了します。

```ruby
Mutations::UpdateAddressMutation = GraphQL::Relay::Mutation.define do
  name 'UpdateAddress'

  input_field :postal_code, !types.Int
  input_field :address, !types.String

  return_field :address, !Types::AddressType

  resolve ->(_obj, inputs, ctx) {
    begin
      address = ctx[:current_user].address
      address.postal_code = inputs.postal_code
      address.address = inputs.address
      address.save
    rescue => e
      return GraphQL::ExecutionError.new(e.message)
    end

    { address: address }
  }
end
```
`{ address: address }`は`return_field`で返す値で、Hashで定義します。また、エラーハンドリングで投げる例外は`GraphQL::ExecutionError`を使うのが良いです。

では、実際にアップデートしてみましょう。graphiqlから以下のクエリを投げてみてください。返ってきた値が更新後のものになっていれば正常に動作しています。

```graphql
mutation {
  updateAddressMutation(input: {
      postal_code: 1638001
      address: "東京都新宿区西新宿２丁目８−１"
  }) {
    address {
      id
      postal_code
      address
    }
  }
}
```


## Mutations::CreatePostMutation
先程定義したQueryの`PostType`を投稿できるようにしてみましょう。Mutationのファイルをgenerateします。

```
$ rails g graphql:mutation CreatePostMutation
```
先程と同じように中身を書いていきます。今度はCreateなので、Postをbuildして値を入れ、保存します。

```ruby
Mutations::CreatePostMutation = GraphQL::Relay::Mutation.define do
  name 'CreatePost'

  input_field :subject, !types.String

  return_field :post, !Types::PostType

  resolve ->(_obj, inputs, ctx) {
    begin
      post = ctx[:current_user].posts.build
      post.subject = inputs.subject
      post.save
    rescue => e
      return GraphQL::ExecutionError.new(e.message)
    end

    { post: post }
  }
end
```

できたらgraphiqlから投稿してみましょう！

```graphql
mutation {
  createPostMutation(input: {subject: "良い感じ！"}) {
    post {
      subject
    }
  }
}
```
current_userのpostsのqueryを投げてみて、postが追加されていることを確認してみてください。

```graphql
{
  user {
    id
    name
    email
    posts {
      edges {
        node {
          id
          subject
        }
      }
    }
  }
}
```

## Mutations::UpdatePostMutation
今度は投稿したpostを更新してみましょう！`UpdatePostMutation`をgenerateします。

```
$ rails g graphql:mutation UpdatePostMutation
```

今度は更新したいpostを指定する必要があるため、`input_field`に`id`を追加します。それ以外はやることは先程とほとんど同じです。

```ruby
Mutations::UpdatePostMutation = GraphQL::Relay::Mutation.define do
  name 'UpdatePost'

  input_field :id, !types.ID
  input_field :subject, !types.String

  return_field :post, !Types::PostType

  resolve ->(_obj, inputs, ctx) {
    begin
      post = ctx[:current_user].posts.find(inputs.id)
      post.subject = inputs.subject
      post.save
    rescue => e
      return GraphQL::ExecutionError.new(e.message)
    end

    { post: post }
  }
end
```
あとは`Types::MutationType`にfieldを追加したら、更新してみましょう！

```graphql
mutation {
  updatePostMutation(input: {id: "{先程投稿したpostのid}", subject: "めっちゃ良い感じ！！！"}) {
    post {
      subject
    }
  }
}
```

## InputType
上記のMutationはinput_fieldの項目が少なかったので良かったのですが、それが多くなってくると全部をMutationに書いていくのはしんどくなってきます。後ほど説明する`description`を追加していくとコードの量が一気に増えます。また、同じモデルに対するCreateやUpdateではinput_fieldが同じになることも多く、使いまわせたら便利です。それをするための仕組みが`InputType`です。試しに`UpdateAddressMutation`のInputTypeを定義してみましょう。新規ファイル`app/graphql/types/address_input_type.rb`に以下のように実装します。

```ruby
Types::AddressInputType = GraphQL::InputObjectType.define do
  name 'AddressInput'

  argument :postal_code, !types.Int
  argument :address, !types.String
end
```
そうしたら、`UpdateAddressMutation`を以下のように変更します。

```ruby
Mutations::UpdateAddressMutation = GraphQL::Relay::Mutation.define do
  name 'UpdateAddress'

  input_field :addressInput, !Types::AddressInputType  # input_fieldにTypes::AddressInputTypeを指定する

  return_field :address, !Types::AddressType

  resolve ->(_obj, inputs, ctx) {
    address_input = inputs.addressInput  # input_fieldのネストが一段深くなるので、addressInputを取り出す
    begin
      address = ctx[:current_user].address
      address.postal_code = address_input.postal_code
      address.address = address_input.address
      address.save
    rescue => e
      return GraphQL::ExecutionError.new(e.message)
    end

    { address: address }
  }
end
```

これで、下記のようにUpdateAddressMutationのクエリを投げることができます。(以前とちょっと違う)

```graphql
mutation {
  updateAddressMutation(input: {addressInput: {postal_code: 1638001, address: "東京都新宿区西新宿２丁目８−１"}}) {
    address {
      id
      postal_code
      address
    }
  }
}
```

# GraphQLのテスト

## Request spec
GraphQLの挙動を確認するテストはrequest specを書いていくのが良さそうです。graphiqlから投げているqueryをテスト内で同じように使うことで、実際の挙動をテストすることができます。

説明はともかく実装を見てみましょう。`spec/requests/graphql/query/user_spec.rb`に以下のようなテストを書きます。

```ruby
require 'rails_helper'

RSpec.describe 'user query', type: :request do
  subject { post graphql_path, params: { query: query } }

  let!(:user) { FactoryBot.create(:user) }

  let(:query) do
    <<~QUERY
      {
        user {
          id
          email
          name
        }
      }
    QUERY
  end

  it 'response body is User data' do
    subject
    json = JSON.parse(response.body, symbolize_names: true)
    expect(json[:data][:user][:id]).to eq user.id
    expect(json[:data][:user][:email]).to eq user.email
    expect(json[:data][:user][:name]).to eq user.name
  end
end
```
これで意図したとおりのQueryが投げられているかを確認できます。ここまで必要ないという意見もあると思いますが、個人的には必要なテストだと思いますし、これが書いてあると後から見た時に挙動が一発で分かるので便利です。Mutationのテストも同じように書くことができます。試してみてください。

## graphql-rubyのschemaが正しいか確認するテスト
graphql-rubyでは、実装からschema.graphqlを機械的に出力することができます。このschema.graphqlはそのままドキュメントになるので、きちんと追従していれば実装とドキュメントがかけ離れるということがありません。追従するに当たってはCIを使って自動更新することも出来ると思うのですが、ここでは簡単に対処するために最新のschemaになっていなかったらテストで落ちるようにするやり方を説明します。

### schema.graphql
その前にschema.graphqlです。先程も言ったとおりこれはドキュメントになるのですが、そのためにはdescriptionを書く必要があります。今までの説明ではすっ飛ばしてきましたが、QueryやMutationを定義する時にdescriptionを追加します。試しに`QueryType`と`UserType`に書いてみましょう。

```ruby
Types::QueryType = GraphQL::ObjectType.define do
  name 'Query'

  field :user, !Types::UserType do
    description 'You can access current_user'  # description を追加
    resolve ->(_obj, _args, ctx) {
      ctx[:current_user]
    }
  end
end
```

```ruby
Types::UserType = GraphQL::ObjectType.define do
  name 'User'

  field :id, !types.ID, 'ユニークな ID'
  field :name, !types.String, '名前'
  field :email, !types.String, 'e-mail アドレス'
  field :address, !Types::AddressType, '住所'
  connection :posts, !Types::PostType.connection_type, '投稿一覧'
end
```

descriptionの書き方はブロックを使うかどうかで変わりますが、やりたいことは同じです。この状態でschema.graphqlをgenerateしてみましょう。まずはgraphql-rubyのrake taskを呼べるようにRakefileに追記します。

```ruby:Rakefile
require_relative 'config/application'
require 'graphql/rake_task'  # 追記

Rails.application.load_tasks

GraphQL::RakeTask.new(schema_name: 'GraphqlRubyDemoSchema')  # 追記
...
```
これでschema.graphqlをdumpできるようになったので、rake taskを実行しましょう。

```
$ rake graphql:schema:dump
```
`schema.graphql`と`schema.json`がdumpされたと思います。`schema.json`の方はschemaの全ての情報がjsonで網羅されています(graphiqlで使われてるんだっけな...よくわかってません)。

schema.graphqlの`type Query`と`type User`を見てみると、先程書いたdescriptionが出力されていることがわかります。この型の定義と説明が合わさったschemaを実装から機械的に生成できるのは、json schemaやswaggerに対して便利な点だと感じています。

### schema.graphqlのテスト
機械的に出力されたschemaも最新でなければ意味がありません。そこで、実装から出力される最新のschemaと既に出力済みのschemaで差分がないかどうかをチェックするテストを書き、最新でなければエラーになるようにします。エラーで落ちたら改めてschemaをdumpすればOKです。以下のようなテストを追加します(私はずぼらなので`spec/requests/graphql/graphql_ruby_demo_schema_spec.rb`に追加しました)。

```ruby
require 'rails_helper'

RSpec.describe 'GraphqlRubyDemoSchema' do
  let(:current_definition) { GraphqlRubyDemoSchema.to_definition }
  let(:printout_definition) { File.read(Rails.root.join('schema.graphql')) }

  it 'equals dumped schema, `rake graphql:schema:dump` please!' do
    expect(current_definition).to eq(printout_definition)
  end
end
```
試しにどこかのタイプを変更してテストを実行してみてください。エラーで落ちたらdumpして再度テストを実行してみてください。テストが通ると思います。

これにより、schemaが古い状態であることに必ず気づくことができ、ドキュメントを常に実装と一致させることができます。また、意図した変更であるかどうかも同時に確認することができます。これ実際に使っていて非常に便利なので、是非使ってみてください！

# 最後に
GraphQLは公式のドキュメントがかなりしっかりしているので、それを読めば必要なことは全部書いてあります。書いてあるのですが割と量が多いので、最初は概要を掴むのに苦労しました。
なので、ざっくり全体像が分かるような記事があると良いなーと思い、これを書きました。最初の入り口として参考になれば幸いです！

graphql-rubyの機能は他にもたくさんありますし、書き方も様々です。まだまだベストプラクティスが定まっていない新しい技術なので、みんなで触ってより良くしていきましょう！

# 参考資料
- http://graphql.org/learn/
- http://graphql-ruby.org/
- https://blog.qnyp.com/2017/06/08/graphql-resources/