graphql-rubyを使ってみて感じたこと、学んだこと

久々に投稿します。

最近業務でgraphql-rubyを使ってみたので、色々と感じたこと、学んだことをまとめようと思いました。
GraphQL自体使うのが初めてだったので、なかなか最初は困惑しましたが、使ってみると便利だなと感じるポイントがたくさんあったので、今後、APIはGraphQLを最優先の選択肢でいきたいなと思いました。

# 環境

Ruby 2.5.1
GraphQL 1.8.7

# 学んだこと、感じたこと

## その① 自分が使っているバージョンのドキュメントをちゃんと読み込むこと

当たり前のことですが、これがめちゃくちゃ大事です。

ちなみに、ドキュメントは[こちら](http://graphql-ruby.org/)

私は、2018年8月時点の最新バージョンである「1.8.7」を使ったのですが、1.8から`class-based API`に変わっていまして、それで大分苦戦を強いられました。
ちなみに1.8が公開されたのは、2018年5月18日です。

具体的にどういうことかというと、QueryやMutationなどの書き方が、全然変わっていました。

```ruby
# 1.7以前

## Mutation
MutationType = GraphQL::ObjectType.define do
  name "Mutation"
  field :createPost, types.Post do
    resolve ->(obj, args, ctx) {
      obj # => #<Organization id=456 ...>
      # ...
    }
  end
end

## Schema定義
ObjectTypes::User = GraphQL::ObjectType.define do
  name "User"
  field :id, !types.ID
  field :name, types.String
  field :posts, ObjectTypes::PostList do
    resolve ->(obj, args, ctx) {
      Post.where(...)
    }
  end
end

```

```ruby
# 1.8以降

## Mutation
class MutationType < GraphQL::Schema::Object
  field :create_post, Post, null: true
  def create_post(**args)
    object # => #<Organization id=456 ...>
    # ...
  end
end

## Schema定義
class UserType < GraphQL::Schema::Object
  field :posts, [ObjectTypes::PostType], null: true

  def posts
    Post.where(...)
  end
end
    
```

マイナーバージョンが上がっただけなのに、この変わりよう、、、
ちなみに、2018年8月の時点では、巷に掲載されている記事は1.7までのもので公開されていたので、それを参考にするとめちゃくちゃつまずきます。

マイナーバージョンでこんなにも書き方が変わった例があると、1.9になったときも変わる可能性は十分にあるので、自分の使っているバージョンのドキュメントを読み込むことはすごく大事です。

_ただ、最新のドキュメントのコード例にも、ちらほらと1.7の記述のものがあったりなかったり。英語に自信がある方はPR出したら通ると思います。_

## その② graphiql-railsは必須

これはもう必須です。
`graphiql-rails`を入れるだけで、Graphiqlを使用することができるため、開発がそれだけで爆速になります。
以下は、GitHubが公開しているGraphiqlです。使用してみると、何が便利かがより分かるかと思います。

Graphiqlのサンプル→https://developer.github.com/v4/explorer/

これからgraphql-rubyを使用して開発しようとしている方は、まずは`grapiql-rails`を入れてください。

## その③ 普通に書いているとN+1問題が多発する

GraphQLの便利さにかまけて、めちゃくちゃ書いていると、こんなことが起きてしまいます。
例えば、以下のようなQueryを投げた場合、確実にN+1問題がおきます。

```
{
  user {
    id
    name
    posts {
      edges {
        node {
          id
          title
        }
      }
    }
  }
}
```

`posts`を取得するところで、N+1になってしまいますね。

これは色々なところで語られていて、`graphql-batch`を使えばいいとか、QueryのFieldsの定義で`includes`を使えばいいとか色々出ています。

[Q: Prevent N+1 queries](https://github.com/rmosolgo/graphql-ruby/issues/189)
[graphql-rubyにおけるN+1対策](https://scrapbox.io/mito/graphql-ruby%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8BN+1%E5%AF%BE%E7%AD%96)

色々と出ていますが、私が担当しているプロジェクトは結構リリースまで時間がなく、追われている状況だったので、最も簡単な方法として、

- 確実にQueryで取得すると分かっているものは、`eager_load`で結合する
- それ以外のものは、`includes`を使って、呼び出されるときに、N+1が起きないようにする

という手法を取りました。
この方法なら、時間もかからず実装できるし、SQLの発行回数が格段に減ります。
特に、開発初期なら、発行されるQueryは全て予想できますからね。

リリース後になると思いますが、できたら以下のようなことをやって、もっとパフォーマンス上げたいですね。

[シンプルさとパフォーマンスを両立した API 設計と実装の一例](https://techblog.heartrails.com/ja/articles/graphql-with-apollo-client)

## その④ ファイルレイアウトは、種類ごとにディレクトリを分ける

こちらは以下の記事を参考にさせていただき、ディレクトリごとにファイルを分けるようにしました。

参考：[RailsでGraphQL APIを作る時に悩んだ5つのこと](https://blog.spacemarket.com/code/graphql-ruby-concerns/)

公式ドキュメントでは、Baseクラスを作成することを推奨していますが、全て`type`ディレクトリ以下に配置していました。
ただ、Modelの数が増えれば増えるほど、`EnumType`や`ScalarType`や`ObjectType`が混在して、見にくくなってきます。

そのため、予め`type`ごとにディレクトリを分けてみました。
ほぼ、上記の参考記事と同様に分けてみました。

```
app/graphql/enum_types/
           /input_types/
           /interface_types/
           /mutations/
           /object_types/
           /scalar_types/
```

ほぼ参考記事通りですが、`input_types`のみ、再利用性だけではなく、見やすさも考慮して、Mutationで更新したいものは、以下のように`InputTypes`にまとめるようにしました。

```ruby
module InputTypes
  class CourseInputType < InputTypes::BaseInputObject
    graphql_name 'CourseInputType'

    argument :name, String, required: true
    argument :content, String, required: true
    argument :price, Integer, required: false
    #...
  end
end
```

もちろん、再利用性も考えていますが、見やすさを一番に考慮してまとめています。

## その⑤ エラーハンドリングの扱い

こちらについては意見が分かれそうですが、私は[公式ドキュメント](http://graphql-ruby.org/mutations/mutation_errors.html)に則って、`errors`フィールドを返す仕様にしてみました。

具体的には、以下のような感じです（公式ドキュメント抜粋）。

```ruby
def resolve(id:, attributes:)
  post = Post.find(id)
  if post.update(attributes)
    {
      post: post,
      errors: [],
    }
  else
    # Convert Rails model errors into GraphQL-ready error hashes
    user_errors = post.errors.map do |attribute, message|
      # This is the GraphQL argument which corresponds to the validation error:
      path = ["attributes", attribute.camelize]
      {
        path: path,
        message: message,
      }
    end
    {
      post: post,
      errors: user_errors,
    }
  end
end
```

GrqphQLはその特性上、エラーコードを返しても、「どこでエラーが起きたのか」がわからないので、基本は200を返して、`errors`にエラーの内容を返すようにしました。
こちらも以下の記事を参考にさせていただきました。

参考：[RailsでGraphQL APIを作る時に悩んだ5つのこと](https://blog.spacemarket.com/code/graphql-ruby-concerns/)

## その⑥ テストはQuery、Mutationごとに書く

これはすごく意見が分かれそうですが、このようにまとめてみました。
公式ドキュメントにも、どのようにspecを書けばいいかが記載されています。

[Executing GraphQL queries](http://graphql-ruby.org/schema/testing.html#executing-graphql-queries)

以下のようにしてQueryのテストをしています（公式ドキュメント抜粋）。

```ruby
RSpec.describe MySchema do
  # You can override `context` or `variables` in
  # more specific scopes
  let(:context) { {} }
  let(:variables) { {} }
  # Call `result` to execute the query
  let(:result) {
    res = MySchema.execute(
      query_string,
      context: context,
      variables: variables
    )
    # Print any errors
    if res["errors"]
      pp res
    end
    res
  }

  describe "a specific query" do
    # provide a query string for `result`
    let(:query_string) { %|{ viewer { name } }| }

    context "when there's no current user" do
      it "is nil" do
        # calling `result` executes the query
        expect(result["data"]["viewer"]).to eq(nil)
      end
    end

    context "when there's a current user" do
      # override `context`
      let(:context) {
        { current_user: User.new(name: "ABC") }
      }
      it "shows the user's name" do
        user_name = result["data"]["viewer"]["name"]
        expect(user_name).to eq("ABC")
      end
    end
  end
end
```

トップレベルの`describe`はQuery名にして、より分かりやすい名前にしました。
また、query、mutationごとにファイルを分けて、以下のような構造でファイルを作成しました。

```
spec/request/graphql/query/course_spec.rb
                           user_spec.rb
                    /mutation/create_course_spec.rb
                              create_user_spec.rb
```

# 総評

色々と最初は戸惑うところもありましたが、使ってみるととても便利で、開発も慣れてくると、`graphiql-rails`のおかげで爆速です。
APIをRailsで開発するのであれば、選択肢として、まずGraphQLを考えてみてもよいかなと思います。

つらつらと書き並べてみましたが、少しでもこれから`graphql-ruby`を使って開発する方にとって参考になれば幸いです。