How to run the Absinthe dataloader only once for all types implementing an interface?

I have an interface in my schema:

interface :authentication_source do
  field(:id, non_null(:id))
  field(:name, non_null(:string))
  field(:slug, non_null(:string))
  field(:user_authentications, list_of(non_null(:user_authentication)))

  resolve_type(fn
    %OidcAuthenticationSource{}, _ -> :oidc_authentication_source
    %MicrosoftAuthenticationSource{}, _ -> :microsoft_authentication_source
  end)
end

as well as 2 nodes types that implement that interface:

node object(:oidc_authentication_source, id_fetcher: &authentication_source_id_fetcher/2) do
  field(:name, non_null(:string))
  field(:slug, non_null(:string))
  field(:metadata_document, :string)
  field(:client_id, :string)
  field(:client_secret, :string)
  field(:user_authentications, list_of(non_null(:user_authentication))) do
    resolve(dataloader(Accounts))
  end

  interface(:authentication_source)
end

node object(:microsoft_authentication_source, id_fetcher: &authentication_source_id_fetcher/2) do
  field(:name, non_null(:string))
  field(:slug, non_null(:string))
  field(:tenant_id, non_null(:string))
  field(:client_id, non_null(:string))
  field(:client_secret, non_null(:string))
  field(:user_authentications, list_of(non_null(:user_authentication))) do
    resolve(dataloader(Accounts))
  end

  interface(:authentication_source)
end

The Accounts source is using Dataloader.Ecto and when I run this GraphQL query:

query authenticationSources {
  authenticationSources {
    __typename
    id
    name
    slug
    ... on OidcAuthenticationSource {
      metadataDocument
      clientId
      clientSecret
    }
    ... on MicrosoftAuthenticationSource {
      tenantId
      clientId
      clientSecret
    }
    userAuthentications {
      externalUserId
      user {
        id
        email
        displayName
      }
    }
  }
}

I observe that 2 identical SQL queries are executed to load the :user_authentications:

  • 1 query with all the IDs of the authentication sources of type :oidc_authentication_source
  • 1 query with all the IDs of the authentication sources of type :microsoft_authentication_source

The dataloader is very helpful but still it is triggering 1 SQL query per type implementing my interface.

I have re-read the following documentations but I cannot find a way to reduce this to a single query :

Is it possible?

1 Like

I discovered that using a key function seems to do the trick:

defp dataloader_user_authentications(authentication_source, _args, _info) do
  %{
    batch: {{:many, UserAuthentication}, %{}},
    item: [authentication_source_dbid: authentication_source.authentication_source_dbid]
  }
end

node object(:oidc_authentication_source, id_fetcher: &authentication_source_id_fetcher/2) do
  field(:name, non_null(:string))
  field(:slug, non_null(:string))
  field(:metadata_document, :string)
  field(:client_id, :string)
  field(:client_secret, :string)
  field(:user_authentications, list_of(non_null(:user_authentication))) do
    resolve(dataloader(Accounts, &dataloader_user_authentications/3))
  end

  interface(:authentication_source)
end

node object(:microsoft_authentication_source, id_fetcher: &authentication_source_id_fetcher/2) do
  field(:name, non_null(:string))
  field(:slug, non_null(:string))
  field(:tenant_id, non_null(:string))
  field(:client_id, non_null(:string))
  field(:client_secret, non_null(:string))
  field(:user_authentications, list_of(non_null(:user_authentication))) do
    resolve(dataloader(Accounts, &dataloader_user_authentications/3))
  end

  interface(:authentication_source)
end

Now a single SQL query is sent. The SQL query looks almost the same as before except it does not duplicate authentication_source_dbid at the end of SELECT and its does not use authentication_source_dbid in ORDER BY.

This was not obvious from the documentation.

@benwilson512 Does that mean it is simply an undocumented feature or it is something that could disappear in future release? Could this behavior be added in the simpler form resolve(dataloader(Accounts)) as well?