graphql-rubyでresolverを使ってqueryをシンプルに保つ

# 概要

graphql-rubyを使っていると、query_type.rbが肥大化してしまう傾向にあります。

GitHubのIssueを見ていたところ、query_type.rbにおいてResolverを使うことで肥大化を(なるべく)回避するベストプラクティスが紹介されていて、公式ガイドにも反映されていなかったようなので共有したいと思います。

参考Issue:
https://github.com/rmosolgo/graphql-ruby/issues/1825#issuecomment-441306410

ちなみに確認したバージョンは以下の通りです。

|ライブラリ|バージョン|
|:--|:--|
|ruby|2.6.5|
|graphql-ruby|1.19.5|

また、GraphQLのレスポンスとしては、ユーザー単体とリストを返却することを想定するものとします。

ユーザーのモデルは以下のイメージです。

```ruby:db/schema.rb
create_table "users", force: :cascade do |t|
  t.string "email"
  t.string "password"
  t.string "first_name"
  t.string "last_name"
  t.datetime "created_at", null: false
  t.datetime "updated_at", null: false
end

```

ちなみに表示に用いるUserTypeは以下を想定しています。

```ruby:app/graphql/types/user_type.rb
module Types
  class UserType < BaseObject
    field :id, Int, null: false
    field :email, String, null: false
    field :password, String, null: false
    field :first_name, String, null: true
    field :last_name, String, null: true
  end
end
```

# 普通のQuery

graphql-rubyにおいて普通にquery_typeを書いていくと、例えばこんな感じになるかと思います。

```ruby:app/graphql/types/query_type.rb
module Types
  class QueryType < BaseObject
    field :users, Types::UserType.connection_type, null: false do
      argument :id, Int, required: false
      argument :email, String, required: false
      argument :first_name, String, required: false
      argument :last_name, String, required: false
    end
    def users(**args)
      res = User.all
      args.each do |k, v|
        # argumentsはGraphQLの仕様上、keyがキャメルケースになる。argsにはスネークケースで入っている
        argument = self.class.arguments[k.to_s.camelize(:lower)]
        # 各argumentで絞り込む
        res = res.where("#{k} = ?", v)
      end
      res
    end

    field :user, Types::UserType, null: false do
      argument :id, Int, required: true
    end
    def user(id:)
      User.find(id)
    end
  end
end
```

出力するデータのカスタマイズをする場合、どうしてもquery_type自身に実装を行なっていく必要があります。

もちろんもう少しシンプルに記載することもできるかもしれませんが、いずれにせよREADに関わる全クエリをquery_type.rbに記載しないといけないため、queryの種類が増えるほど見通しが悪くなっていきます。

# Resolverを使ったQuery

Queryには`resolve`オプションがありますが、これを用いて処理をResolverに投げることで処理を外出しします。

```ruby:app/graphql/types/query_type.rb
module Types
  class QueryType < BaseObject
    field :users, resolver: Resolvers::UserConnectionResolver
    field :user, resolver: Resolvers::UserResolver
  end
end
```

query_type.rbに関して言えばだいぶシンプルで見通しが良くなりました。

当然ながら、具体的に何を返すかについてはResolverで実装が必要です。
とはいえquery_type.rbにあった処理をただResolver側に移すだけになります。

```ruby:app/graphql/resolvers/user_connection_resolver.rb
module Resolvers
  class UserConnectionResolver < GraphQL::Schema::Resolver
    type UserType.connection_type, null: false

    argument :id, Int, required: false
    argument :email, String, required: false
    argument :first_name, String, required: false
    argument :last_name, String, required: false

    def resolve(**args)
      res = User.all
      args.each do |k, v|
        argument = self.class.arguments[k.to_s.camelize(:lower)]
        res = res.where("#{k} = ?", v)
      end
      res
    end
  end
end
```

```ruby:app/graphql/resolvers/user_resolver.rb
module Resolvers
  class UserResolver < GraphQL::Schema::Resolver
    type UserType, null: false

    argument :id, Int, required: true

    def resolve(id:)
      User.find(id)
    end
  end
end
```

このように処理をResolverとして切り出すことで、query_type.rbをGraphQLのルーティングを行うファイルにして、各resolver.rbでController的に処理を実装していくといった構成にすることができます。

# Resolverの使用は注意が必要？

Resolverのパターンを紹介しましたが、[公式ガイド](https://graphql-ruby.org/fields/resolvers.html)では本当にResolverを使う必要があるか？とResolverの使用に懐疑的なようです。

- テストがしづらくなる
- graphql-rubyの更新
- Resolverが肥大化していってしまう

といったことが懸念視されているようです。

ただここで紹介したテクニックに関しては、参考Issue内で作者も良いパターンだと認めておりドキュメントの更新をしてくれという風にコメントしているので、今後ガイド側も更新されるかもしれません。

どんなケースでもResolverを使えばいいというわけではなさそうなので、他のケースに当てはめるときは注意が必要そうです。

ということでresolverを使ったパターンの紹介でした。