【Rails6】GraphQLを使用したAPI開発(Query編)

## はじめに
rails6でGraphQLを用いた開発を行ったので、導入方法や使い方をまとめてみました。
今回はQuery/アソシエーション/N+1問題を取り上げて、ご紹介します。

## 開発環境
ruby2.7.1
rails6.0.3
GraphQL

## 1. GraphQLって何？
GraphQLとはAPIリクエストのためクエリ言語で下記の特徴があります。

- エンドポイントは`/graphql`1つのみ
- Query: データの取得(Get)
- Mutation: データの作成、更新、削除(Create, Update, Delete)

RESTとの大きな違い一つ目のエンドポイントが一つということです。
RESTの場合、`/sign_up`, `/users`, `/users/1`など複数のエンドポイントが存在しますが、
GraphQLの場合は、エンドポイントは`/graphql`のみです。

RESTでは複数のリソースで必要な場合、複数のAPIリクエストが必要ですが、
GraphQLはエンドポイントが一つなので、<font color="Red">必要なデータを一回で取得でき</font>コードがシンプルになります。


## 2. 今回使用するテーブル
親のUserテーブルと子のPostテーブルの二つで実装します。
Userが複数のPostを投稿できる1対多の関係とします。

```:ターミナル 
$ rails g model User name:string email:string
$ rails g model Post title:string description:string user:references
$ rails db:migrate
```

### Userテーブル

|カラム|型|
|----|----|
|name|string|
|email|string|

```user.rb
class User < ApplicationRecord
  has_many :posts, dependent: :destroy
end
```

### Postテーブル

|カラム|型|
|---|---|
|title|string|
|description|string|

```post.rb
class Post < ApplicationRecord
  belongs_to :user
end
```


## 3. railsにGraphQLを導入する
gemをインストールします。

```ruby:Gemgile
gem 'graphql'    #追加

group :development, :test do
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
  gem 'graphiql-rails'   #開発環境に追加
end
```

```config/application.rb
require "sprockets/railtie"    #コメントアウトを外す
```

```:ターミナル
$ bundle install
$ rails generate graphql:install   #GraphQLに関するファイルが作成されます
```


`routes.rb`に下記を追加
エンドポイントが開発環境では`/graphiql`, 本番環境では`/graphql`となります。

```routes.rb
Rails.application.routes.draw do
  if Rails.env.development?
    # add the url of your end-point to graphql_path.
    mount GraphiQL::Rails::Engine, at: "/graphiql", graphql_path: "/graphql" 
  end
  post '/graphql', to: 'graphql#execute'  #ここはrails generate graphql:installで自動生成される
end
```

詳細は下記にまとめましたので参考にしてください。
[Rails6のAPIモードでGraphQLを使う方法(エラー対策も含む)](https://qiita.com/shungo_m/items/5ebd1c87379111e0d524)


## 4. User一覧を取得するQuery
GraphQLではType(modelごとに定義する)を持っており、そのTypeに従い、Queryを実行してデータを取得します。

### UserのObjectTypeを作成
下記のコマンドでUserのTypeを作成します。
`!`をつけると`null:false`が追加されます。

```:ターミナル 
$ rails g graphql:object User id:ID! name:String! email:String!
```

下記のファイルが生成されます。

```user_type.rb
module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false  # `!`をつけると`null:false`が追加されます。
    field :name, String, null: false
    field :email, String, null: false
  end
end
```


### UserのQueryを作成
先ほど作成した`user_type`を元にqueryを作成します。

```query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: false # userを配列で定義する
    def users
      User.all # user一覧を取得
    end
  end
end
```


Userのデータをコンソールで作成します。

```
$ rails c
$ > User.create(name: "user1", email: "user-1@test.com")
$ > User.create(name: "user2", email: "user-2@test.com")
```

### Queryを実行する
準備は整いましたので、サーバーを立ち上げてGraphiqlで確認します。(http://localhost:3000/graphiql)


```
$ rails s
```


下記のqueryを実行します。

```
query{
  users{
    id
    name
    email
  }
}
```

するとjson形式のレスポンスが返ってきます。
usersのTypeを配列にしているので配列となっています。

```
{
  "data": {
    "users": [
      {
        "id": "1",
        "name": "user1",
        "email": "user-1@test.com"
      },
      {
        "id": "2",
        "name": "user2",
        "email": "user-2@test.com"
      }
    ]
  }
}
```
http://localhost:3000/graphiql に接続して、実行した結果です。
![スクリーンショット 2020-09-27 20.32.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/497683/390f358a-4fbc-4529-8058-6c4e9839a457.png)

### 必要なデータだけリクエストし受け取る場合
すべてカラムのデータが必要でない場合はqueryを変更します。
例えばuserのidだけ取得することもできます。

```
query{
  users{
    id
  }
}
```

レスポンス

```
{
  "data": {
    "users": [
      {
        "id": "1",
      },
      {
        "id": "2",
      }
    ]
  }
}
```


## 5. アソシエーション
次にUserに紐づくPostを取得してみます。
Userを同じようにObjectTypeとQueryを作成します。

### PostのObjectTypeを生成

```:ターミナル 
$ rails g graphql:object Post id:ID! title:String! description:String!
```

下記のファイルが生成されます。
Userのデータを取得するために`field :user, Types::UserType, null: false`を追加します。

```post_type.rb
module Types
  class PostType < Types::BaseObject
    field :id, ID, null: false
    field :title, String, null: false
    field :description, String, null: false
    field :user, Types::UserType, null: false # この一文を追加。belongs_to :userのようなもの
  end
end
```

UserTypeには`field :posts, [Types::PostType], null: false`を追加します。
こちらは複数データが紐づくので配列で定義します。

```user_type.rb
module Types
  class UserType < Types::BaseObject
    field :id, ID, null: false
    field :name, String, null: false
    field :email, String, null: false
    field :posts, [Types::PostType], null: false # この一文を追加。has_many :postsのようなもの
  end
end
```

```:ターミナル
$ rails c
$ > Post.create(title: "title", description: "description", user_id: 1)
```

### PostのQueryを生成する

```query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: false
    def users
      User.all
    end

    # 下記を追加
    field :posts, [Types::PostType], null: false
    def posts
      Post.all
    end
  end
end
```

### Queryを実行する

実行するqueryです。
usersにpostをネストさせてリクエストします。

```
query{
  users{
    id
    name
    email
    post{
      id
      title
      description
    }
  }
}
```

実行するとネストしたPostのデータが返ってきます。
RESTの場合でいう`user.posts`のデータを取得できます。

![スクリーンショット 2020-09-27 21.00.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/497683/d020d705-4cbd-1725-9d46-3284822bd3bb.png)


`post.user`のデータが必要な場合は、下記のようなqueryで取得できます。

```
query{
  posts{
    id
    title
    description
    user{
      id
    }
  }
}
```

![スクリーンショット 2020-09-28 9.54.16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/497683/0effe265-7832-e10a-c58e-89328fa25f0e.png)


## 6. N+1を検知するBulletを導入する
GraphQLのクエリは木構造になっているので、アソシエーションがあるとN+1問題が発生しやすいです。
そこで、N+1を検知するgemであるBulletを導入することをおすすめします。

### Bulletをインストール

```ruby:Gemfile
group :development do
  gem 'bullet'
end
```

```:ターミナル
$ bundle install
```

config/environments/development.rbに下記の設定を追加

```config/environments/development.rb
config.after_initialize do
  Bullet.enable = true
  Bullet.alert = true
  Bullet.bullet_logger = true
  Bullet.console = true
  Bullet.rails_logger = true
end
```

### N+1を確認してみる
Bulletの導入が完了したら、N+1が発生していないか確認してみましょう

```
query{
  users{
    id
    name
    email
    posts{
      id
      title
      description
    }
  }
}
```

先ほどと同様のqueryを実行すると下記のようなログが出ます。

```:ターミナル
Processing by GraphqlController#execute as */*
  Parameters: {"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil, "graphql"=>{"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil}}
  User Load (0.2ms)  SELECT "users".* FROM "users"
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
  Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 1]]
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
  Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 2]]
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
Completed 200 OK in 36ms (Views: 0.3ms | ActiveRecord: 1.7ms | Allocations: 18427)


POST /graphql
USE eager loading detected
  User => [:posts]
  Add to your query: .includes([:posts])
Call stack
```

`Add to your query: .includes([:posts])`と言われているのでN+1が発生しています。
SQLも三回発行されています。

```:ターミナル
User Load (0.2ms)  SELECT "users".* FROM "users"
Post Load (0.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 1]]
Post Load (0.1ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = ?  [["user_id", 2]]
```

### N+1を解消する方法
N+1を解消するには通常通り、`includes`すれば大丈夫です。
dataloaderというものでも解消できますが、今回は`includes`を使用します。
User一覧を取得している部分を下記のように変更します。

```query_type.rb
def users
  # User.all  # 変更前
  User.includes(:posts).all # 変更後
end
```

では、N+1が解消されたかログで確認してみましょう。
警告がなくなっています。

```:ターミナル
Processing by GraphqlController#execute as */*
  Parameters: {"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil, "graphql"=>{"query"=>"query{\n  users{\n    id\n    name\n    email\n    posts{\n      id\n      title\n      description\n    }\n  }\n}", "variables"=>nil}}
  User Load (0.7ms)  SELECT "users".* FROM "users"
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
  Post Load (1.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (?, ?)  [["user_id", 1], ["user_id", 2]]
  ↳ app/controllers/graphql_controller.rb:15:in `execute'
Completed 200 OK in 57ms (Views: 0.2ms | ActiveRecord: 1.9ms | Allocations: 15965)
```

SQL文も2つに減ったので無事にN+1が解消されました。

```:ターミナル
User Load (0.7ms)  SELECT "users".* FROM "users"
Post Load (1.2ms)  SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (?, ?)  [["user_id", 1], ["user_id", 2]]
```


## 7. resolverの切り出し
### 通常のQuery
普通にコードを書いていくと、モデルに関係なく、fieldsとメソッドが`query_type`に追加されるので`query_type`がどんどん肥大化します。

```query_type.rb
module Types
  class QueryType < Types::BaseObject
    field :users, [Types::UserType], null: false
    def users
      User.includes(:posts).all
    end

    field :posts, [Types::PostType], null: false
    def posts
      Post.all
    end
  end
end
```

### Resolverを使ったQuery
GitHubのissueでは、Resolverを使用することで、query_type.rbの肥大化を回避するベストプラクティスが紹介されています。
https://github.com/rmosolgo/graphql-ruby/issues/1825#issuecomment-441306410

`query_type`にはfieldのみを定義します。

```query_type.rb
module Types
  class QueryType < BaseObject
    field :users, resolver: Resolvers::QueryTypes::UsersResolver
    field :posts, resolver: Resolvers::QueryTypes::PostsResolver
  end
end
```

そして、メソッドの部分はObjectTypeごとにResolverに切り出します。(新たにresolversディレクトリを作成しました。)
`GraphQL::Schema::Resolver`の記載を忘れるとエラーが出るので忘れないように注意してください。

```resolvers/query_types/users_resolver.rb
module Resolvers::QueryTypes
  class UsersResolver < GraphQL::Schema::Resolver
    type [Types::UserType], null: false
    def resolve
      User.includes(:posts).all
    end
  end
end
```

```resolvers/query_types/posts_resolver.rb
module Resolvers::QueryTypes
  class PostsResolver < GraphQL::Schema::Resolver
    type [Types::PostType], null: false
    def resolve
      Post.all
    end
  end
end
```

## 終わりに
GraphQLついて、自分の復習もかねてまとめていたら思いの外、長文となってしまいました。
rspecやmutationについても書きたかったですが、次回にしたいと思います。