graphqlelixirectodataloaderabsinthe

How can I get Absinthe and Dataloader to work together?


I have a GraphQL API that works just fine using conventional resolve functions. My goal is to eliminate the N+1 problem.

To do so I've decided to use the Dataloader. I've done these steps to supposedly make the app run:


  1. I added these two functions to my context module:
defmodule Project.People do
  # CRUD...

  def data, do: Dataloader.Ecto.new(Repo, query: &query/2)

  def query(queryable, _params) do
    queryable
  end
end
  1. I added context/1 and plugins/0 to the Schema module and updated the resolvers for queries:
defmodule ProjectWeb.GraphQL.Schema do
  use Absinthe.Schema

  import Absinthe.Resolution.Helpers, only: [dataloader: 1]

  alias ProjectWeb.GraphQL.Schema
  alias Project.People

  import_types(Schema.Types)

  query do
    @desc "Get a list of all people."
    field :people, list_of(:person) do
      resolve(dataloader(People))
    end

    # Other queries...
  end

  def context(context) do
    loader =
      Dataloader.new()
      |> Dataloader.add_source(People, People.data())

    Map.put(context, :loader, loader)
  end

  def plugins, do: [Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
end

No other steps are given in the official tutorials. My :person object looks like this:

@desc "An object that defines a person."
  object :person do
    field :id, :id
    field :birth_date, :date
    field :first_name, :string
    field :last_name, :string
    field :pesel, :string
    field :second_name, :string
    field :sex, :string

    # field :addresses, list_of(:address) do
    #   resolve(fn parent, _, _ ->
    #     addresses = Project.Repo.all(Ecto.assoc(parent, :addresses))

    #     {:ok, addresses}
    #   end)
    #   description("List of addresses that are assigned to this person.")
    # end

    # field :contacts, list_of(:contact) do
    #   resolve(fn parent, _, _ ->
    #     contacts = Project.Repo.all(Ecto.assoc(parent, :contacts))

    #     {:ok, contacts}
    #   end)
    #   description("List of contacts that are assigned to this person.")
    # end
  end

The commented part is the resolver that works without dataloader and doesn't cause the problem.

When I try to query:

{
  people { 
    id
  }
}

I get this:

Request: POST /graphiql
** (exit) an exception was raised:
    ** (Dataloader.GetError)   The given atom - :people - is not a module.

  This can happen if you intend to pass an Ecto struct in your call to
  `dataloader/4` but pass something other than a struct.

I don't fully comprehend the error message since I pass a module to the dataloader/1 and cannot find the solution. What might be the case?


Solution

  • I've managed to get this to work - here's how:

    The dataloader doesn't know by itself what to pull from the database, it only understands the associations. Thus dataloader(People) can only be a part of an object block, not the query block.

    In other words:

    defmodule ProjectWeb.GraphQL.Schema do
      use Absinthe.Schema
    
      import Absinthe.Resolution.Helpers, only: [dataloader: 1]
    
      alias ProjectWeb.GraphQL.Schema
      alias Project.People
    
      import_types(Schema.Types)
    
      query do
        @desc "Get a list of all people."
        field :people, list_of(:person) do
          resolve(&StandardPerson.resolver/2)
        end
    
        # Other queries...
      end
    
      def context(context) do
        loader =
          Dataloader.new()
          |> Dataloader.add_source(People, People.data())
    
        Map.put(context, :loader, loader)
      end
    
      def plugins, do: [Absinthe.Middleware.Dataloader | Absinthe.Plugin.defaults()]
    end
    

    and

      @desc "An object that defines a person."
      object :person do
        field :id, :id
        field :birth_date, :date
        field :first_name, :string
        field :last_name, :string
        field :pesel, :string
        field :second_name, :string
        field :sex, :string
    
        field :addresses, list_of(:address) do
          resolve(dataloader(People))
          description("List of addresses that are assigned to this person.")
        end
    
        field :contacts, list_of(:contact) do
          resolve(dataloader(People))
          description("List of contacts that are assigned to this person.")
        end
      end