既存のRailsプロジェクトにGraphQL APIを追加してみた

遅くなりましたが、 [PORT Advent Calendar](https://qiita.com/advent-calendar/2018/port) 12 日目 ~~13日目になってしまった~~ 担当の 18 卒の山内です。
[PORT 株式会社](https://www.theport.jp/)で、サーバーサイドエンジニアとして開発を行っています。

うまくまとまっていないので、追加・修正していく予定です。
間違っているところがあれば、 `コメント` もしくは `編集リクエスト` をお願いします。

# なぜ GraphQL について書くか :writing_hand:

入社してすぐ、新卒エンジニアで集まって行った LT 会で GraphQL を少し触った内容を話したのですが、それからなかなか手を付けられてなかったので、今回のアドベントカレンダーをきっかけにもう一度挑戦しようと思いました。

# GraphQL とは :question:

- API への問合せ言語 (クエリ言語)
- Facebook 製
- 1 回のリクエストで必要なものだけ取得、更新できる
- エンドポイントが 1 つ
- 破壊的な変更が起こらないので、 `v1` や `v2` といったバージョンを付けなくて良い
  - 参考： [GraphQL を最速でマスターするための意識改革３ヶ条](https://qiita.com/jabba/items/8d77ab86641937847673)
- `REST API` の代わり？

## Query

- データ取得
- `REST API` の `GET` 相当のもの

## Mutation

- データ更新
- `REST API` の `POST`, `PUT`, `DELETE` 相当のもの

## Subscription

- サーバでのデータの変化をクライアントに通知？

# どこで使われてるの :question:

- Facebook
- GitHub
  - https://developer.github.com/v4/
- Twitter
- Pinterest

# 既存のプロジェクトに GraphQL を入れる

> graphql-ruby はただの GraphQL 処理系で、ウェブアプリケーション全体に影響を及ぼすものではありません。Rails 的に言えば、 `GraphqlController#execute` というアクションを追加するだけですし、これすら `rails generate graphql:install` で雛形を生成できます。したがって、すでに RESTful API で運用している Rails アプリにも GraphQL API を足すことは簡単ですし、それから徐々に移行していけばいいのです。

参考： [既存の Rails アプリに GraphQL API を足す - Notes on GraphQL for Ruby](https://scrapbox.io/graphql-ruby-ja/%E6%97%A2%E5%AD%98%E3%81%AERails%E3%82%A2%E3%83%97%E3%83%AA%E3%81%ABGraphQL_API%E3%82%92%E8%B6%B3%E3%81%99)

と書かれているとおり、追加しても既存のプロジェクトには何の影響も与えないので安心して追加できそうです。

# `gem` のインストール

まず、初めに `GraphQL` の `gem` をインストールします。

```Gemfile:Gemfile
gem 'graphql'
```

```zsh:zsh
$ bundle install
```

次に、 `GraphQL` のセットアップをしましょう。

```zsh:zsh
$ bundle exec rails g graphql:install
 Running via Spring preloader in process 33086
       create  app/graphql/types
       create  app/graphql/types/.keep
       create  app/graphql/sample_app_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_scalar.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"
      gemfile  graphiql-rails
        route  graphiql-rails
 Gemfile has been modified, make sure you `bundle install`
```

## GraphiQL

`grahiql-rails` という `gem` を追加することで動作確認を簡単に行なえます。

`$ bundle exec rails g graphql:install` した際に、 `graphiql-rails` が `Gemfile` に追加されているので、

```zsh:zsh
$ bundle install
```

ここで、今回利用した gem のバージョンを確認しておきます。

```zsh:zsh
$ bundle exec gem list graph

*** LOCAL GEMS ***

graphiql-rails (1.5.0)
graphql (1.8.11)
```

`$ bundle exec rails g graphql:install` を実行した際に、 `graphiql` 用のルーティングも追加されているので、

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

  post "/graphql", to: "graphql#execute"
  ~~
end
```

サーバーを立ち上げて、 `localhost:3000/graphiql` にアクセスしましょう。

アクセスすると、以下のような画面が表示されます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/59569/1ab32476-0140-3af5-b334-050bad07b3d0.png)

この画面で実装後、すぐに動作確認できます。
また、右上の `Docs` でドキュメントの確認もできます。

すでに、テスト用の `test_field` が定義されているので実行してみましょう。

```ruby:app/graphql/types/query_type.rb
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"
    def test_field
      "Hello World!"
    end
  end
end
```

 下記のような `Query` を投げることで、 `"Hello World!"` が返って来ました。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/59569/01a5728b-74b2-9040-a819-f381ba7a3010.png)

# Query

次は、自分が欲しい情報を得られる `Query` を投げられるようにしていきましょう。

> これから説明する中で、 `ShopType` や `BrandType` などを定義します。
> すでに、既存の Rails アプリの `ActiveRecord` のモデルとして `Shop`、 `Brand` が作成された状態だと仮定して進めるので、それぞれの Rails アプリのモデルに合わせて読み進めてください。

まず、`object` を作成します。

```zsh:zsh
$ rails g graphql:object Shop id:ID! name:String! access:String!
```

上記コマンドを実行すると `app/graphql/types/shop_type.rb` が作成されます。
`id:ID!` のようにすると `null: false` に、 `name:String` のように `!` を付けない場合は `null: true` になります。

```ruby:app/graphql/types/shop_type.rb
module Types
  class ShopType < Types::BaseObject
    description '店舗'

    field :id, ID, '店舗ID', null: false
    field :name, String, '店舗名', null: false
    field :access, String, 'アクセス', null: true
    field :open_time, String, null: false
    field :close_time, String, null: false
  end
end
```

`null: true`、 `null: false` は Rails のモデルのカラムが `null` を許容するかどうかに合わせると良いでしょう。

また、次の `SampleType` の `field` のように, 説明を書くと `Docs` に追加されます。

```ruby:app/graphql/types/sample_type.rb
module Types
  class SampleType < Types::BaseObject
    description '説明説明'

    field :field_name1, '説明1', null: false
    field :field_name2, null: false, description: '説明2'
  end
end
```

![sample](https://i.gyazo.com/9826de32ea39b1a9a1f376fdbd04711a/raw)

さきほど作成した `ShopType` を使って `Query` で呼び出せるように `QueryType` に `shop` を定義します。

`argument` には、引数として利用したいものを定義します。今回は 1 つですが、複数定義できます。

```ruby:app/graphql/types/query_type.rb
module Types
   class QueryType < Types::BaseObject
     field :shop, ShopType, null: true do
       description '店舗をidで検索'
       argument :id, ID, '店舗ID', required: true
     end

     def shop(id:)
       Shop.find(id)
     end
   end
 end
```

以下のような `Query` を投げると対応した `JSON`が返ってきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/59569/bc558b77-c303-8d4e-3c67-637c06489935.png)

`id` を引数として渡さない場合は、最後の Shop を返すようにしてみました。

```ruby:app/graphql/types/query_type.rb
module Types
   class QueryType < Types::BaseObject
     field :shop, ShopType, null: true do
       description '店舗をidで検索'
       argument :id, ID, '店舗ID', required: false # id がなくても検索できるように
     end

     def shop(id: nil) # id を渡さないときには nil を渡す
       id ? Shop.find(id) : Shop.last # idがなければ最後の Shop を返す
     end
   end
 end
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/59569/5d884da2-bc1e-d5a7-2aa0-17a8523e56d2.png)

### 型

`field` の型は以下のものから指定できます。
ID, String, Fixnum, Integer, Float, Date, Time, DateTime, Array, Object, Hash

> Date, Time, DateTime は、前回触ったときは使えませんでした。
> 今後、試してみます。

自分で定義することも可能です。

参考 [graphql-rails/lib/graphql/rails/types.rb](https://github.com/jamesreggio/graphql-rails/blob/6d1f3718adedde18d10abf75332581eadff22aa3/lib/graphql/rails/types.rb#L92)

---

`BrandType` も `ShopType` と同じ様に作成してみました。

```zsh:zsh
$ rails g graphql:object Brand id:ID! name:String!
```

ちなみに、Rails のモデルやコントローラーと同じ様に `rails d` で消せます

```zsh:zsh
$ rails d graphql:object Brand id:ID! name:String!
```

```ruby:app/graphql/types/brand_type.rb
module Types
  class BrandType < Types::BaseObject
    description 'ブランド'

    field :id, ID, 'ブランドID', null: false
    field :name, String, 'ブランド名', null: false
  end
end
```

## `1:1` のアソシエーションの場合

Shop と Brand が `1：1` のときに、

```ruby:app/models/shop.rb
class Shop < ApplicationRecord
  belongs_to :brand, required: true
  has_many :reviews, dependent: :destroy
  ~~
end
```

```ruby:app/models/brand.rb
class Brand < ApplicationRecord
  has_many :shops, dependent: :destroy
  has_many :reviews, through: :shops, dependent: :destroy
  ~~
end
```

`ShopType` の `field` の型を `BrandType` とすると、

```ruby:app/graphql/types/shop_type.rb
module Types
  class ShopType < Types::BaseObject
    description '店舗'

    ~~
    field :brand, BrandType, 'ブランド', null: false
  end
end
```

次のように、呼び出せます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/59569/9908b112-6d19-cef2-bad2-18e60a4af981.png)

## `1:N` のアソシエーションの場合

Shop と Review が `1:N` のとき、

```ruby:app/models/shop.rb
class Shop < ApplicationRecord
  ~~
  has_many :reviews, dependent: :destroy
  ~~
end
```

```ruby:app/models/review.rb
class Review < ApplicationRecord
  ~~
  belongs_to :shop
  ~~
end
```

```zsh:zsh
$ be rails g graphql:object Review
```

```ruby:app/graphql/types/review_type.rb
module Types
  class ReviewType < Types::BaseObject
    description 'レビュー'

    field :id, ID, 'レビューID', null: false
    field :reviewer_name, String, null: false
    field :shop, ShopType, '店舗', null: false
  end
end
```

`ShopType` の `field` の型を `[ReviewType]` とすると、

```ruby:app/graphql/types/shop_type.rb
module Types
  class ShopType < Types::BaseObject
    description '店舗'

    ~~
    field :reviews, [ReviewType], 'レビュー', null: false
  end
end
```

次のように呼び出せます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/59569/493d6fcd-51b1-e72f-c1cc-317b063d141c.png)

## Shop を店舗名であいまい検索してみる

```ruby:app/graphql/types/query_type.rb
module Types
  class QueryType < Types::BaseObject
    ~~
    field :shops, [ShopType], null: true do
      description '店舗名であいまい検索'
      argument :name, String, '店舗名', required: true
    end

    def shops(name:)
      Shop.where('name LIKE ?', "%#{name}%").limit(10)
    end
    ~~
  end
end
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/59569/07154514-88bd-54d5-23b8-7e1d024fdbc8.png)

### Shop モデルに定義されているインスタンスメソッドを呼んでみる

```ruby:app/models/shop.rb
class Shop < ApplicationRecord
  ~~

  # 全レビューの総合点平均
  def overall_score
    ~~
  end
end
```

`method` も `field` に定義すれば `Query` で呼ぶことができます。

```ruby:app/graphql/types/shop_type.rb
module Types
  class ShopType < Types::BaseObject
    description '店舗'

    ~~
    field :overall_score, Float, '全レビューの総合点平均', null: true, method: :overall_score
  end
end
```

スネークケースで定義した `field` は、`GraphQL` ではキャメルケースで呼ぶようになっていました。
`overall_score` は `overallScore` になります。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/59569/b5bb99c7-4f49-1d9d-a3f7-2f207e1b2632.png)

## 一度に複数の情報を取得

以下のように、同時に複数の情報を取得できます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/59569/d4445749-b584-7333-dfd7-326ab6c7e9af.png)

# Mutation

前のバーションの情報が多くて、よくわからなかったので今後調べて書きます。

# まとめ

- 楽しい :dancer_tone1: 
- 結構簡単に実装できる :sunglasses:
- 以前触ったときからの変更が多い :joy:
- `description` を書いておくと勝手に `Docs` に追加される :thumbsup:

# 参考 :bow:

- [GraphQL - Welcome](http://graphql-ruby.org/)
- [Notes on GraphQL for Ruby](https://scrapbox.io/graphql-ruby-ja/)
- [雑に始める GraphQL Ruby【class-based API】](https://qiita.com/vsanna/items/031aa5a17a2f284eb65d)
- [GraphQL Ruby の使い方 (基礎編)](https://qiita.com/sukechannnn/items/b2168d7214992e5d37bf)
- [GraphQL を最速でマスターするための意識改革３ヶ条](https://qiita.com/jabba/items/8d77ab86641937847673)

# よくわかっていないところ :innocent:

わかり次第追記します。

- Mutation

- `app/graphql/types/query_type.rb` が増えてきたら読みづらそう

  - 分割すれば良い :question:

- この辺

  - `app/graphql/types/base_enum.rb`
  - `app/graphql/types/base_input_object.rb`
  - `app/graphql/types/base_interface.rb`
  - `app/graphql/types/base_eobject.rb`
  - `app/graphql/types/base_scaler.rb`
  - `app/graphql/types/base_union.rb`

- `$ rails g graphql:xxxx`

- `N+1` 問題がよく起きるらしい

- client
  - apollo

# 最後に :santa:

[PORT 株式会社](https://www.theport.jp/)では、自社サービスを支えてくれる優秀な Ruby エンジニアを募集しています(Ruby エンジニア以外も)。
[もくもく会](https://freestyle-mokumoku.connpass.com/)も行なっていますので、ぜひ一緒にもくもくしましょう！

[PORT Advent Calendar](https://qiita.com/advent-calendar/2018/port) はまだまだ続きます。乞うご期待！