absinthe-graphql / dataloader

DataLoader for Elixir
MIT License
489 stars 99 forks source link

run_batch override for association loads #133

Closed giddie closed 2 years ago

giddie commented 3 years ago

It would be really nice if we could override batch loading for associations.

Imagine a fusebox with fuses. In my absinthe schema, I specify:

field :fuses, resolve: dataloader(Fusebox)

I can specify a base query in the context module:

defmodule Fusebox
  def dataloader_source() do
    Dataloader.Ecto.new(Repo, query: &dataloader_query/2)
  end

  defp dataloader_query(Fuse, %{} = _params) do
    Fuse |> Fuse.Query.base()
  end
end

However, imagine that later, we decide that for our domain model, a fusebox cannot contain zero fuses, so if none are found for a given Fusebox, we should return a single default fuse. I would now like to override run_batch to override the way this association is loaded, but there is no facility to do that. So this cannot be done from within the context module without changing the API offered to the schema resolvers. Instead I have to change the schema to something like this:

field :fuses, resolve: fn fusebox, _args, %{context: %{loader: loader}} ->
  Dataloader.load(loader, Fusebox, {:one, Fusebox.Fuse}, fusebox: fusebox)
  |> on_load(fn loader ->
    Dataloader.get(loader, Fusebox, {:one, Fusebox.Fuse}, fusebox: fusebox)
  end)
end

Which will then use the "standard" implementation of run_batch that will delegate to the run_batch hook in the context module. This seems like an important limitation of the "magic" association data loading functionality, pushing a change that should affect only the domain logic into the schema layer. If I need to change the way I load the data in order to modify the loading behaviour in the context, it makes me wary of using the association shortcut magic at all :(

tlvenn commented 3 years ago

Hi @giddie,

You should be able to do this by leveraging the callback option of the dataloader helper.

giddie commented 3 years ago

Hi @tlvenn. Thanks, but that's the problem - it pushes responsibility for a domain logic concern right down into the schema resolution layer. This really should be handled in the context module.

For now I've decided to bypass the magic association helper in the schema and use Dataloader directly, handling all association batch loading at the context level (with some generic helpers). That way the context module can have the final say over all data flow.

I definitely feel that @jfrolich was onto something here: https://github.com/absinthe-graphql/dataloader/issues/25#issuecomment-374890495

benwilson512 commented 2 years ago

I would now like to override run_batch to override the way this association is loaded

You can do this:


  def run_batch(Fusebox.Fuse, query, :fuses, fuse_boxes, repo_opts) do
    # run batch boilerplate to load things by association

    # return a [default_goes_here] for each `fuse_box` that returns no items.
  end

  def run_batch(queryable, query, col, inputs, repo_opts) do
    Dataloader.Ecto.run_batch(Repo, queryable, query, col, inputs, repo_opts)
  end