概要
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のレスポンスとしては、ユーザー単体とリストを返却することを想定するものとします。
ユーザーのモデルは以下のイメージです。
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は以下を想定しています。
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を書いていくと、例えばこんな感じになるかと思います。
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に投げることで処理を外出しします。
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側に移すだけになります。
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
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のパターンを紹介しましたが、公式ガイドでは本当にResolverを使う必要があるか?とResolverの使用に懐疑的なようです。
- テストがしづらくなる
- graphql-rubyの更新
- Resolverが肥大化していってしまう
といったことが懸念視されているようです。
ただここで紹介したテクニックに関しては、参考Issue内で作者も良いパターンだと認めておりドキュメントの更新をしてくれという風にコメントしているので、今後ガイド側も更新されるかもしれません。
どんなケースでもResolverを使えばいいというわけではなさそうなので、他のケースに当てはめるときは注意が必要そうです。
ということでresolverを使ったパターンの紹介でした。