回 各投稿に紐づくコメントの取得クエリ:投稿の数(N) 回実行 GraphQL の構造上、何も対策をしないとすぐにN+1 問題にぶつかる SELECT * FROM comments WHERE post_id = 1 SELECT * FROM comments WHERE post_id = 2 SELECT * FROM posts Comment Comment Comment Comment Comment Post Post Post root query { posts { title body comments { name body } } }
確かにN+1 は回避できるが、client 側が利用するfield を決定するGraphQL では厳しい → 今ではDataloader と呼ばれるアプローチが主流になっている Now that we see the problem, what can we do about this? There are multiple ways to look at the problem. The first one is to ask ourselves if we could not find a way to load data ahead of time, instead of waiting for child resolvers to load their small part of data. In this case, this could mean for the friends resolver to “look ahead” and see that the best friend will need to be loaded for each. It could then preload this data and each bestFriend resolver could simply use a part of this preloaded data. This solution is not the most popular one, and that’s understandable. A GraphQL server will usually let clients query data in the representation they like. This means our loading system would need to adapt to every single scenario of data requirements that could appear very far into a query. It is definitely doable, but from what I’ve seen so far, most solutions out there are quite naive and will eventually break in very complex data loading scenarios. Instead, the more popular approach at the moment is one that is commonly called “DataLoader”. This is because the first implementation of this pattern for GraphQL was released as a JavaScript library called DataLoader. [1] 1. https://book.productionreadygraphql.com/
be used as part of your application’s data fetching layer to provide a simplified and consistent API over various remote data sources such as databases or web services via batching and caching. データソースからのデータ取得にあたって「バッチ処理機構」と「キャッシュ機構」を提供する仕組み 主なユースケースとしてGraphQL があるというだけで、GraphQL のためだけの仕組みではないというのが ポイント💡 このリファレンス実装をもとに、他の言語では各言語の仕様に合わせてOSS が提供されている Elixir だとabsinthe-graphql/dataloader で提供されている README にも「facebook/dataloader にインスパイアされて実装し、Elixir に適するように変更を加え た」と書いてある Dataloader provides an easy way efficiently load data in batches. It’s inspired by https://github.com/facebook/dataloader, although it makes some small API changes to better suit Elixir use cases. [1] 1. https://github.com/graphql/dataloader
do field :id, non_null(:id) field :title, non_null(:string) field :body, non_null(:string) field :comments, non_null(list_of(:comment)) do resolve(fn post, _, _ -> # 各post からcomments を毎回取得している # これだとN+1 が発生する comments = Ecto.assoc(post, :comments) |> DataloaderSample.Repo.all() {:ok, comments} end) end end # ... (中略) query do field :posts, non_null(list_of(non_null(:post))) do resolve(&BlogResolver.list_posts/3) end end end
def data() do # Dataloader.Ecto.new/2 でDataloader.Ecto 構造体を生成 Dataloader.Ecto.new(DataloaderSample.Repo, query: &query/2) end # query/2 関数のパターンを増やすことでクエリ実行時の条件分岐を実装可能 def query(queryable, _params) do queryable end # 例. def query(Post, %{has_admin_rights: true}), do: Post def query(Post, _), do: from p in Post, where: is_nil(p.deleted_at) def query(queryable, _), do: queryable # ... end
だとContext ごとにsource を作ると良い In a Phoenix application you’ll generally have one source per context, so that each context can control how its data is loaded. plugins に追加してResolution 時にバッチ処理が実行されるようにする resolver をdataloader のヘルパー関数を用いて実装する Dataloader.Ecto.new でsource を作成する query オプションを変更して実行されるクエリを制御する source≒queue と捉えるとしっくりくるかも? Phoenix であればContext ごとにqueue を作り、resolution のタイミングでqueue がflush される(= バッチが実行 される) イメージ [1] [2] ` ` ` ` 1. https://hexdocs.pm/absinthe/dataloader.html 2. 実際に実装を見るとイメージが湧きやすい