Rails + GraphQLでAPI作成

## 各バージョン

```
ruby: 2.7.1
rails: 6.0.3.4
graphql-ruby: 1.11.6
```

## GraphQL Ruby

[公式ページ](https://graphql-ruby.org/)

RailsでGraphQLを扱う場合↑のgemを使ってAPIを実装していきます。

### graphiql-rails
合わせて [graphiql-rails](https://github.com/rmosolgo/graphiql-rails) gemを入れておくとブラウザ上で実装したGraphQLの
確認ができるIDEが使えるようになります :sparkles: 
※ `graphql-ruby`のinstall時に `graphiql-rails` のgemをGemfileに追加してくれます

イメージ画像
<img width="500" alt="graphiql-rails" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/ac80adb1-76ed-8851-ce62-6489eeb6b98e.png">

###  :computer:環境構築
****

```ruby:Gemfile
gem 'graphql'
gem 'graphiql-rails' # 今回は先に入れました
```

gemがインストールされたら `rails generate graphql:install` コマンドを実行し各ファイルを生成します。
生成されたファイルは以下の通り↓

```sh
$ rails generate graphql:install
      create  app/graphql/types
      create  app/graphql/types/.keep
      create  app/graphql/app_schema.rb
      create  app/graphql/types/base_object.rb
      create  app/graphql/types/base_argument.rb
      create  app/graphql/types/base_field.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/mutations/base_mutation.rb
      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
```

この時点での `routes.rb` は以下のようになっています。

```ruby
Rails.application.routes.draw do

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

  post '/graphql', to: 'graphql#execute'
end
```
### :pencil: 実装
****

#### Query 作成
まずは各テーブルに対応するTypeを定義しないといけないので、
例として以下の `users` テーブルに対応する `user_type` を作成してみたいと思います。

```
create_table :users do |t|
  t.string :name, null: false
  t.string :email
  t.timestamps
end
```

以下コマンドを実行すると `user_type` が作成されます。
(指定する型は `ID` がGraphQLで定義されているid用の型です(実態はString)
また語尾に `!` が付いているものはnullを許容しない型となり、`!` が付いてないものはnull許容になります。)

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

【補足】既にDBにテーブルが存在している場合はよろしくやってくれるっぽいので

```sh
$ bundle exec rails g graphql:object User
```
↑これでも大丈夫でした :sparkles: 

生成されたファイル `graphql/type/user_type.rb` は以下のようになっていました。

```ruby
module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :email, String, null: true
    field :created_at, GraphQL::Types::ISO8601DateTime, null: false
    field :updated_at, GraphQL::Types::ISO8601DateTime, null: false
  end
end
```

既に生成されている `graphql/type/query_type.rb` に以下を追加します。

```ruby
    field :users, [Types::UserType], null: false
    def users
      User.all
    end
```

`http://localhost:3000/graphiql`上で以下クエリを投げるとレスポンスが返ってくるかと思います。

```graphql
{
  users {
    id
    name
    email
  }
}
```

#### Mutationsの作成
次にユーザーを作成するMutations `CreateUser` を作成してみたいと思います。

```sh
$ bundle exec rails g graphql:mutation CreateUser
```
`graphql/mutations/create_user.rb` が作成されるので、以下の様に修正します。

```ruby
module Mutations
  class CreateUser < BaseMutation
    field :user, Types::UserType, null: true

    argument :name, String, required: true
    argument :email, String, required: false

    def resolve(**args)
      user = User.create!(args)
      {
        user: user
      }
    end
  end
end
```

既に生成されている `graphql/types/mutation_type.rb` に以下を追記します。

```ruby
module Types
  class MutationType < Types::BaseObject
    field :createUser, mutation: Mutations::CreateUser # 追記
  end
end
```

`http://localhost:3000/graphiql`上で以下を実行するとUserが作成されます。

```graphql
mutation {
  createUser(
    input:{
      name: "user"
      email: "user@email.com"
    }
  ){
    user {
      id
      name 
      email
    }
  }
}
```

#### Association

- 1:1の関連テーブルの場合

例として `Post` が `Label` と1:1で関連付されている場合

```ruby:label_type.rb
module Types
  class LabelType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    ...
  end
end
```

```ruby
module Types
  class PostType < Types::BaseObject
    field :label, LabelType, null: true
  end
end
```
↑の様に `label` を `LabelType` として定義できます。
この場合の Query のイメージとしては

```graphql
{
  posts {
    id
    label {
      id
      name
    }
  }
}
```
上記の様に `label` を `LabelType` として必要な値をQueryできます。

- 1:Nの関連テーブルの場合

例として `User` が `Post` と1:Nの場合

```ruby
module Types
  class PostType < Types::BaseObject
    field :id, ID, null: false
    field :label, LabelType, null: true
  end
end
```

```ruby
module Types
  class UserType < Types::BaseObject
    field :posts, [PostType], null: false
  end
end
```

上記の様に `posts` を `[PostType]` として定義でき、Queryとしては

```graphql
{
  user(id: 1234) {
    id
    posts {
      id
      label {
        id
        name
      }
    }
  }
}
```
↑の様に呼び出す事ができます。

#### graphql-batch

↑の説明の様に 1:1や1:Nの関連テーブルのデータも取ってくる事ができますが
今のままだとDBへの問い合わせが大量に発生してしまう場合があります。
`User` が `Post` と1:Nの場合の例で `Post` が100件ある場合、それぞれ100回問い合わせが発生してしまいます。

そこで解決方法の一つである複数問い合わせをまとめやってくれる [graphql-batch](https://github.com/Shopify/graphql-batch) を導入してみます。

```ruby
gem 'graphql-batch'
```

Gemをインストールしたら、`loader` を作成していきます。
`loader` は「複数問い合わせをまとめる」部分の実装になります。

```ruby:graphql/loaders/record_loader.rb
module Loaders
  class RecordLoader < GraphQL::Batch::Loader
    def initialize(model)
      @model = model
    end

    def perform(ids)
      @model.where(id: ids).each { |record| fulfill(record.id, record) }
      ids.each { |id| fulfill(id, nil) unless fulfilled?(id) }
    end
  end
end
```
これを先程の `Post` が `Label` と1:1で関連付されている場合に適用すると

```ruby
module Types
  class PostType < Types::BaseObject
    field :label, LabelType, null: true
    def label
      Loaders::RecordLoader.for(Label).load(object.label_id)
    end
  end
end
```
こんな感じで書けます。
`User` が `Post` と1:Nの場合には別途loaderを作成します。

```ruby:graphql/loaders/association_loader.rb
module Loaders
  class AssociationLoader < GraphQL::Batch::Loader
    def self.validate(model, association_name)
      new(model, association_name)
      nil
    end

    def initialize(model, association_name)
      @model = model
      @association_name = association_name
      validate
    end

    def load(record)
      raise TypeError, "#{@model} loader can't load association for #{record.class}" unless record.is_a?(@model)
      return Promise.resolve(read_association(record)) if association_loaded?(record)
      super
    end

    # We want to load the associations on all records, even if they have the same id
    def cache_key(record)
      record.object_id
    end

    def perform(records)
      preload_association(records)
      records.each { |record| fulfill(record, read_association(record)) }
    end

    private

    def validate
      unless @model.reflect_on_association(@association_name)
        raise ArgumentError, "No association #{@association_name} on #{@model}"
      end
    end

    def preload_association(records)
      ::ActiveRecord::Associations::Preloader.new.preload(records, @association_name)
    end

    def read_association(record)
      record.public_send(@association_name)
    end

    def association_loaded?(record)
      record.association(@association_name).loaded?
    end
  end
end
```
※ loaderはgraphql-batchのリポジトリにサンプルがあるので、そちらを参考にして実装すると良さそうです

以下の様に書くと、まとめて問い合わせしてくれるようになります。

```ruby
module Types
  class UserType < Types::BaseObject
    field :posts, [PostType], null: false
    def posts
      Loaders::AssociationLoader.for(User, :posts).load(object)
    end
  end
end
```

#### スキーマファイルからドキュメント生成
最後に定義したスキーマファイルから良い感じのドキュメントを自動で生成するようにしてみたいと思います。

`routes.rb` にマウントできてデプロイ毎に自動でgraphdocが更新される
便利なgemを探していたら[graphdoc-ruby](https://github.com/alpaca-tc/graphdoc-ruby)というgemがあったので試してみます。

`Gemfile` に以下を追加

```ruby
gem 'graphdoc-ruby'
```
また、npmパッケージの[@2fd/graphdoc](https://github.com/2fd/graphdoc)も必要なので
予めDockerイメージ内でインストールしておきます。(Docker使用してない場合はローカル環境にインストールすれば良いかと思います)

例)

```Dockerfile
RUN set -ex \
    && wget -qO- https://deb.nodesource.com/setup_10.x | bash - \
    && apt-get update \
    && apt-get install -y \
                 ...
                 --no-install-recommends \
    && rm -rf /var/lib/apt/lists/* \
    && npm install -g yarn \
    && npm install -g @2fd/graphdoc # インストールしとく
```

`config/routes.rb` に以下を追記

```ruby:config/routes.rb
Rails.application.routes.draw do
  mount GraphdocRuby::Application, at: 'graphdoc'
end
```

※ エンドポイントを変更している場合、`config/initializers/graphdoc.rb` を修正する

例)

```ruby
GraphdocRuby.configure do |config|
  config.endpoint = 'http://0.0.0.0:3000/api/v1/graphql'
end
```

Railsを再起動して、http://localhost:3000/graphdoc でドキュメントが生成されればOKです :sparkles: 

<img width="800" alt="graphdoc" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/299271/58e9aaa2-2fd7-ebbc-be86-c9906da8a21e.png">

###  :bomb: バッドノウハウ
****
-  `http://localhost:3000/graphiql` アクセス時に以下エラーが発生する場合

    ```
    Sprockets::Rails::Helper::AssetNotPrecompiled in GraphiQL::Rails::Editors#show
    ```
    - 解決方法1

        `app/assets/config/manifest.js` に以下を追加する

        ```
        //= link graphiql/rails/application.css
        //= link graphiql/rails/application.js
        ```
        [AssetNotPrecompiled error with Sprockets 4.0 · Issue #75 · rmosolgo/graphiql-rails](https://github.com/rmosolgo/graphiql-rails/issues/75#issuecomment-546306742)
        -> ただこれだとProduction時に `Sprockets::FileNotFound: couldn't find file 'graphiql/rails/application.css'` エラーが出て使えない...

    - **解決方法2 （うまくいった方法）**

        `gem 'sprocket'のバージョン3.7.2に下げる`

        ```ruby
        gem 'sprockets', '~> 3.7.2'  [#1098:  slowdev/knowledge/ios/FirebaseをCarthageで追加する](/posts/1098) 
        ```
        ↑を追加し、`bundle update`
        [Rails6のAPIモードでGraphQLを使う方法(エラー対策も含む) - Qiita](https://qiita.com/shungo_m/items/5ebd1c87379111e0d524)

- graphiqlの画面に`TypeError: Cannot read property 'types' of undefined` が表示される
    -> 手元の環境だとRails再起動で治りました

- graphiqlの画面に`SyntaxError: Unexpected token < in JSON at position 0` が表示される
    -> エラーが発生してる可能性がるのでログを見て修正する

### :link: 参考になったURL
****
- [【Rails】graphql-rubyでAPIを作成 - Qiita](https://qiita.com/k-penguin-sato/items/07fef2f26fd6339e0e69)
- [REST APIが主流のプロジェクトの中でGraphQLを導入してみた話（サーバーサイド編） - Sansan Builders Blog](https://buildersbox.corp-sansan.com/entry/2020/07/17/110000)
- [「GraphQL」徹底入門 ─ RESTとの比較、API・フロント双方の実装から学ぶ - エンジニアHub｜若手Webエンジニアのキャリアを考える！](https://eh-career.com/engineerhub/entry/2018/12/26/103000)
- [GraphQLを使ったAPI仕様中心開発の導入とその効果の紹介 - Kaizen Platform 開発者ブログ](https://developer.kaizenplatform.com/entry/laco/2018-06-08)
- [雑に始める GraphQL Ruby【class-based API】 - Qiita](https://qiita.com/vsanna/items/031aa5a17a2f284eb65d)
- [hawksnowlog: Ruby (Sinatra) で GraphQL 入門](https://hawksnowlog.blogspot.com/2019/06/graphql-with-sinatra.html)
- [既存のRailsプロジェクトにGraphQL APIを追加してみた - Qiita](https://qiita.com/yamotech/items/cc8c3c1d9f0baad16a97#11-%E3%81%AE%E3%82%A2%E3%82%BD%E3%82%B7%E3%82%A8%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E5%A0%B4%E5%90%88)
- [Ruby on Rails で sprockets から Webpacker へ移行し、移行できないものは共存させる方法 - Qiita](https://qiita.com/tatsurou313/items/645cbf0a3af4c673b5df)
- [Reading: 初めてGraphQL - 型の基礎｜tkhm｜note](https://note.com/tkhm_dev/n/n83efc181f63d)
- https://github.com/loopstudio/rails-graphql-api-boilerplate
- https://github.com/rmosolgo/graphql-ruby-demo