dataloader icon indicating copy to clipboard operation
dataloader copied to clipboard

Dataloader loading incorrect association when struct Id is null

Open arunr14 opened this issue 1 year ago • 3 comments

Encountered a scenario where dataloader is associating the incorrect child object to a parent object. I was able to work around it by explicitly specifying the foreign key relationship in the resolver.

Environment

  • Elixir version : Elixir 1.16.2 (compiled with Erlang/OTP 26)
  • Absinthe version: Absinthe 1.6.8
  • Client Framework and version : Apollo

ISSUE

I have a graphql object called task with a field checklist that is resolved using dataloader. I have two ecto schemas called task and recurringTask with both of them having a field checklist_id. I have a custom resolver that queries both tables, combines the data and returns a struct %task{ :id, :recurring_id, :checklist_id}. In the returned list, it is possible for some entries to have id = null. When this happens dataloader associates the entry with and incorrect checklist object i.e. task.checklist_id != task.checklist.id. Other entries in the list have task.checklist_id = task.checklist.id as expected. However, for entries with id = null dataloader seems to associate a random checklist object from the ones it has fetched.

I was able to workaround this issue by explicitly specifying the foreign key in the resolver.

Schema/Code

ECTO schema

schema "tasks" do
   <other fields>
    belongs_to :checklist, Tasks.Checklist
    
schema "recurring_tasks" do
    <other fields>
    belongs_to :checklist, Tasks.Checklist

GRAPH QL Definitions

field :tasks, non_null_list(:task) do
      resolve &Queries.list_tasks/3
end
object :task do
    ecto_fields Tasks.Task
    field :checklist, :checklist, resolve: dataloader(Tasks)
end

WORKAROUND:

object :task do
    ecto_fields Tasks.Task
    field :checklist, :checklist, resolve: fn parent, _, %{context: %{loader: loader}} -> 
      loader
      |> Dataloader.load(Tasks, {:one, Tasks.Checklist}, id: parent.checklist_id)
      |> on_load(fn loader ->
        loader
        |> Dataloader.get(Tasks, {:one, Tasks.Checklist}, id: parent.checklist_id)
        |> (&{:ok, &1}).()
    end)
  end
end

arunr14 avatar May 23 '24 15:05 arunr14

I noticed the same is true when the struct doesn't even have an ID.

fuelen avatar Aug 02 '24 13:08 fuelen

If having a primary key is a strong requirement, then dataloader must early raise an exception. Or generate an internal ID by itself

fuelen avatar Aug 02 '24 13:08 fuelen

@fuelen I have opened a PR that adds such raise: https://github.com/absinthe-graphql/dataloader/pull/177

@benwilson512 What do you think about that?

szymonkozak avatar May 06 '25 12:05 szymonkozak