自分が担当のプロダクトのWebAPIを整理していかんとほんまヤバいなぁ〜と思いつつ、数年過ごしていましたが、ここ最近で急激にGraphQL熱が湧き上がり、今お試しで実装していってる最中です。
ビビッときたのは、@gfxさんの書かれたGraphQL徹底入門の記事からです。
これを元に簡単な実装をしてみて、他の情報を探して…という感じ。
N+1にはgraphql-batch
概要を掴んだら、ModelにマッピングするようにTypeを生成していってたのですが、N+1問題が発生しやすいみたいな話を聞いていた通り、すぐ出始めました。graphql-batchを使えば、発生が抑えられるとのことだったので、情報を集めてやってみたところ、N+1は収まりました。
preloadして、極力クエリの発行を抑えるようになっています。ありがたい。
ちなみに最初は『preloadやeager_loadを使えば別にN+1起きないんじゃない?』と思って、Foo.preload(:bar)
とかしていたのですが、これだとbarの情報にアクセスしていなくてもbarの情報を取得するためのクエリが実行されてしまいました(N+1にはならないけれど、無駄なクエリ)。graphql-batchは、barの情報にアクセスするときだけ、barをpreloadしてくれたので、graphql-batchを使った方がいいでしょう。
Loaderの実装は、サンプルからそのまま拝借しました。
graphql-batch/examples at 058213b78775c791135bf7db784b7d10007d5ade · Shopify/graphql-batch · GitHub
Loaderの記述がしんどい
とはいえ、Loaderの記述が多くなると、同じようなコードになってしまい辛いです。
module Types class HogeType < Types::BaseObject field :id, ID, null: false field :piyos, [Types::PiyoType], null: false field :fugas, [Types::FugaType], null: false # piyosもfugasも殆ど同じ… def piyos Loaders::AssociationLoader.for(Hoge, :piyos).load(object) end def fugas Loaders::AssociationLoader.for(Hoge, :fugas).load(object) end end end
というわけでメタプログラミングします。継承元のTypes::BaseObject
にクラスメソッドpreload_associations
を追加しました。やってることは、引数のシンボル名のメソッドを定義してしまうということだけです。
module Types class BaseObject < GraphQL::Schema::Object def self.preload_associations(*assciations) model_name = self.class_name[0..-5] # HogeTypeのTypeを削除する model = Kernel.const_get model_name # model Hoge を取得 assciations.each do |association_name| define_method association_name do Loaders::AssociationLoader.for(model, association_name).load(object) end end end end end
これにより、preload_assciationsを呼べば済むようになりました🎉
module Types class HogeType < Types::BaseObject field :id, ID, null: false field :piyos, [Types::PiyoType], null: false field :fugas, [Types::FugaType], null: false # これでOK preload_associations :piyos, :fugas end end
配列型のせいでN+1発生もLoaderで解決
PostgreSQLの配列型を一部で使っているのですが、これもN+1問題が発生しました。配列型ではRailsのAssociationを表現しているわけではないので、自分でLoadしてあげないといけません。
module Types class HogeType < Types::BaseObject field :id, ID, null: false # これらはAssociation field :piyos, [Types::PiyoType], null: false field :fugas, [Types::FugaType], null: false preload_associations :piyos, :fugas # これらは配列型 field :foos, [Types::FooType], null: false field :bars, [Types::BarType], null: false # 配列型はload_manyを使って登録しておくとよい def foos Loaders::RecordLoader.for(Foo).load_many(object.foo_ids) end def bars Loaders::RecordLoader.for(Bar).load_many(object.bar_ids) end end end
今の所の感想
graphqlの実装していくのは楽しい!!graphiqlからクエリ発行して芋づる式にデータが取れるのがめちゃくちゃ面白いです。ただし、クエリのネストが深くなると途端に重くなりますが…。 まだ更新系のクエリの実装はしていないので、そちらでも知見が貯まったら何か書こうと思います。