はじめに
GraphQLの利点として「必要なデータを一括取得できる」という点があると思います。
RESTfulなAPIとの違いという意味も含めて、
今回はqueryを組み合わせて依存関係にあるデータを一括取得する方法を書きたいと思います。
あくまで設計指針という内容なので、具体的な環境構築方法は趣旨からはずれるため割愛します。
使うもの
- 言語: Ruby2.7
- Gem: graphql-ruby, graphql-rails
やりたいこと
例えば以下の2つのqueryを同時に呼びたいとします
{
customer(name: "hoge") {
id
}
}
{
invoice(customerId: 1, status: "open") {
price
}
}
ですが、customerIdを知るにはcustomer queryを叩かなければならないため、
愚直にやるとcustomer queryを叩いてcustomerIdを取得してからinvoice queryを叩く必要があると思います。
なら以下のようにすればいいと思うかもしれません。
{
customer(name: "hoge") {
id
invoice(status: "open") {
price
}
}
}
しかし、これだと「customerIdを知っている状態でinvoice queryを単体で呼びたい時」に困ります。
せっかくcustomerIdを知っている状態でcustomer queryを呼ぶとOver-fetchingになりますよね。
そのため、queryはqueryで残したいと思います。
解決策
以下のようにcustomer queryのinvoice fieldにcustomerIdを渡すことでqueryをネストしたようにできます。
{
customer(name: "hoge") {
id
name
# invoice queryと同じ
invoice(customerId: null, status: "open") {
price
}
}
}
# query単体でも叩ける
{
invoice(customerId: 1, status: "open") {
price
}
}
メリット
このようにすることで、queryをそのままobject typeのfieldに充てることができます。
具体的にはこの後の実装を見ていただきたいです。
実装の具体例
今回はgraphql-rubyを使用します。
query typeは以下のように定義します。
module Types
class QueryType < Types::BaseObject
field :customer, resolver: Resolvers::CustomerResolver
field :invoice, resolver: Resolvers::InvoiceResolver
end
end
graphql-rubyでは基本的にresolverをクラス単位で作成します。
module Resolvers
class CustomerResolver < BaseResolver
type Types::Object::CustomerType, null: true
def resolve(name:)
Customer.find_by(name: name)
end
end
end
ここでCustomerTypeのinvoice fieldにもresolverを充てます。
module Types::Object
class CustomerType < Types::BaseObject
field :id, ID, null: false
field :name, String, null: false
# queryと同じように定義できる
field :invoice , resolver: Resolvers::InvoiceResolver
end
end
InvoiceResolverはというと
object(Customerインスタンス)からinvoiceをたどることも
引数のcustomer_idからinvoiceを取得することもできるようにします。
module Resolvers
class InvoiceResolver < BaseResolver
type Types::Object::InvoiceType, null: true
argument :customer_id, ID, required: false
argument :status, String, required: true
def resolve(customer_id:, status:)
raise ArgumentError if object.nil? && product_id.nil?
# 引数の有無で判定
customer_id ? Invoice.find_by(customer_id: customer_id) : object.invoice
end
end
end
まとめ
これでqueryとqueryをネストできているはずです。
このようにquery同士も組み合わせることができるところがGraphQLの魅力かと思いました。
リクエストを一つにまとめることもできますし、customer_idのように一度保持した値から単体でクエリを叩いて必要な分だけ取得するというNot Over-fetchingなところが強力ですね。