profile
viewpoint

Ask questionsResolution.Helpers.dataloader/3 use_parent option does not work

Environment

  • Elixir version (elixir -v): Elixir 1.7.3 (compiled with Erlang/OTP 21)
  • Absinthe version (mix deps | grep absinthe): absinthe 1.4.16, dataloader 1.0.6
  • Client Framework and version (Relay, Apollo, etc): Apollo

Expected behavior

  • Define graphql schema objects as follows:
  import Absinthe.Resolution.Helpers, only: [dataloader: 3]
  
  object  :organization do
      field :users, list_of(:portal_login) do
            # Define some input fields (for example for filtering/sorting
            # Define a resolver that eagerly joins the person record for portal_logins (e.g. to filter or sort by name)
      end
  end  

  object :portal_login do
    field :person, :person, resolve: dataloader(Person, :person, use_parent: true)
  end
  • The parent object, organization, needs to eagerly load a child object's association, in order filter by it. In our case, we want to maybe allow filtering the list of users based on the name that's stored within the portal_login > person association
  • Dataloader helper for the child object should be able to simply use the already loaded person association in the parent when we use the use_parent: true option

Actual behavior

When a query like the following is run:

query {
   organization {
       users {
           person {
              name
           }
       }
    }
}

The following error is seen.

** (exit) an exception was raised:
    ** (Dataloader.GetError) "Unable to find batch {:assoc, Hapi.Accounts.PortalLogin, #PID<0.702.0>, :person, Hapi.Accounts.Person, %{}}"
        (dataloader) lib/dataloader.ex:198: Dataloader.do_get/2
        (hapi) lib/hapi_web/schema/types/accounts.ex:144: anonymous fn/6 in HapiWeb.Schema.Types.Accounts.do_dataloader/6
        (hapi) lib/hapi_web/schema/middleware/dataloader.ex:39: HapiWeb.Schema.Middleware.Dataloader.get_result/2
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:209: Absinthe.Phase.Document.Execution.Resolution.reduce_resolution/1
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:168: Absinthe.Phase.Document.Execution.Resolution.do_resolve_field/4
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:153: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:72: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:257: Absinthe.Phase.Document.Execution.Resolution.build_result/4
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:153: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:72: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:98: Absinthe.Phase.Document.Execution.Resolution.walk_results/6
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:87: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:257: Absinthe.Phase.Document.Execution.Resolution.build_result/4
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:153: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:72: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:257: Absinthe.Phase.Document.Execution.Resolution.build_result/4
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:153: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:72: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:257: Absinthe.Phase.Document.Execution.Resolution.build_result/4
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:153: Absinthe.Phase.Document.Execution.Resolution.do_resolve_fields/6
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:72: Absinthe.Phase.Document.Execution.Resolution.walk_result/5
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:53: Absinthe.Phase.Document.Execution.Resolution.perform_resolution/3
        (absinthe) lib/absinthe/phase/document/execution/resolution.ex:24: Absinthe.Phase.Document.Execution.Resolution.resolve_current/3
        (absinthe) lib/absinthe/pipeline.ex:274: Absinthe.Pipeline.run_phase/3
        (absinthe_plug) lib/absinthe/plug.ex:421: Absinthe.Plug.run_query/4
        (absinthe_plug) lib/absinthe/plug.ex:247: Absinthe.Plug.call/2
        (phoenix) lib/phoenix/router/route.ex:39: Phoenix.Router.Route.call/2
        (phoenix) lib/phoenix/router.ex:275: Phoenix.Router.__call__/1

If I don't set the use_parent option and use the Absinthe.Resolution.Helpers.dataloader/1 variant OR use the 3 arity variant, but set use_parent: false, the query works properly. However, I can see in the Ecto logs that after the eagerly joined person table, I see dataloader doing a separate query on the person table to load the person records. So, it's using the already loaded object from the parent association.

Relevant Code/Information

I've tried to debug what's happening and I've narrowed it down to the following.

  • Absinthe.Resolution.Helpers.dataloader/3 creates a resolver, which calls do_dataloader/6 link
  • do_dataloader/6 uses use_parent/6 helper to put the resolved association in the loader's results
  • do_dataloader/6 eventually calls on_load/2 to setup the Absinthe.Middleware.Dataloader and provide it the loader that already has the preloaded association in the result
  • So far so good :)
  • When resolution happens for the portal_login.person field, Absinthe.Middleware.Dataloader.call/2 link gets called. I believe this is the function where things start to go wrong.
  • Absinthe.Middleware.Dataloader.call/2 has the following check !Dataloader.pending_batches?(loader), which looks to see whether the batches is non-empty for the dataloader source. When use_parent is false, this check will fail, association loading is getting batched up. Consequently, the else block will execute and update the loader within the resolution object with the one supplied by the Absinthe.Resolution.Helpers.on_load/2 function earlier.
        %{
          resolution
          | context: Map.put(resolution.context, :loader, loader),
            state: :suspended,
            middleware: [{__MODULE__, callback} | resolution.middleware]
        }
  • However, for the use_parent: true case, the !pending_batches? check will pass, and it will call get_result(resolution, callback) on the current resolution object, without updating the loader within the context with the results included within the loader supplied by Absinthe.Resolution.Helpers.on_load/2. Therein lies the problem and the reason why the dataloader is complaining that the batch key cannot be found. It can't be found because the loader within the resolution context doesn't have the result, only the preloaded loader supplied by the Absinthe.Resolution.Helpers.on_load/2 to the Absinthe.Middleware.Dataloader has that.

Taking a step back

If I haven't barked up the wrong tree with my debugging, it seems that fixing this issue isn't a simple matter of using the loader supplied in the arguments to Absinthe.Middleware.Dataloader. If we did that, whenever use_parent is set, it would expect the association to be present within the parent object and fail, if it's not there.

I think a more powerful solution would be to make use_parent: true function as giving priority to the object association being loaded from the parent, if it's already loaded. Otherwise, defaulting to loading the association separately. To support this, we'd have to merge the results set between the loader within the resolution context and the one supplied as an arg to Absinthe.Middleware.Dataloader.

Looking through the dataloader project it appears that adding the ability to merge result sets would need to be part of the Dataloader.Source protocol, in order for each source to do the right thing for merging its result sets.

It has been a fascinating journey discovering all of this 😄. If I'm lucky, I was completely wrong and I'm doing something stupid with my setup. If not, I'd be interested in your thoughts about this issue.

absinthe-graphql/absinthe

Answer questions bruce

May be related to the details around this old use_parent PR: #557

useful!
source:https://uonfu.com/
answerer
Bruce Williams bruce @github Portland, OR http://linktr.ee/brucewilliams Polyglot programmer, co-creator of Absinthe, the GraphQL toolkit for Elixir.
Github User Rank List