How do I implement Many to Many relationship with Absinthe Dataloader

In my phoenix application, I have a many to many relationship between an Artist and a Cause schema implemented using a join table artists_causes. In my Artists schema, I have many_to_many :causes, Cause, join_through: "artists_causes" and in the causes schema I have many_to_many :artists, Artist, join_through: "artists_causes"
I am using absinthe for graphql and in my CauseTypes module, I have implemented a the cause object as below

```
defmodule MyAppWeb.Schema.CauseTypes do
  @moduledoc """
  All types for causes
  """
  use Absinthe.Schema.Notation
  import Absinthe.Resolution.Helpers, only: [dataloader: 1, dataloader: 3]
  
  object :cause do
    field :id, :id
    field :artists, list_of(:artist), resolve: dataloader(Artists)
  end

  def dataloader do
    alias MyApp.{Artists, Causes}
    loader = Dataloader.new
    |> Dataloader.add_source(Causes, Causes.datasource())
    |> Dataloader.add_source(Artists, Artists.datasource())
  end

  def context(ctx) do
    Map.put(ctx, :loader, dataloader())
  end

  def plugins do
    [Absinthe.Middleware.Dataloader] ++ Absinthe.Plugin.defaults()
  end
end```

From my understanding, with Absinthe Dataloader, the dataloader/1 is what I need to have the list of Artists loaded. However, I am not able to query for the artists from within a cause getting the error artists: #Ecto.Association.NotLoaded<association :artists is not loaded> when I run the query below in graphiql

```
query{
 causes{
  id
  artists {
            id
   }
 }

}```

Is there any little piece that I am missing on working with many to many relationships?


stackoverflow

I’ve not yet encountered many_to_many relationships in combination with dataloader but that’s also because it generally makes more sense to use a has_many relationship with through set, and make the join table a separate schema.

If at a later stage you want to add columns to the join table it is much easier to add.

1 Like

Can you show me the body of this function?

many_to_many and has_many through: are not the same thing and depending on your use case one or the other should be used.

One example is:

In fact, given :through associations are read-only,

many_to_many also allows you to create a join schema.

:join_through - specifies the source of the associated data. It may be a string, like "posts_tags", representing the underlying storage table or an atom, like MyApp.PostTag, representing a schema. This option is required.

https://hexdocs.pm/ecto/Ecto.Schema.html#many_to_many/3
https://hexdocs.pm/ecto/Ecto.Schema.html#has_many/3-has_many-has_one-through

I’ve tested with my setup and it should work

# collection.ex
many_to_many, :products, Product, join_through: Collect
iex> loader
|> Dataloader.load(:db, :products, collection)
|> Dataloader.run()
|> Dataloader.get(:db, :products, collection)
[ ... ]

Hi @hlx @maartenvanvliet Thanks for comments, First mistake I made before I asked the first question above was that I had not preloaded my Artist schema in the query list_causes() method in Causes context, I updated my list_causes() function from

def list_causes do    
   Repo.all(MyApp.Causes.Cause) 
end

to

def list_causes do
    Repo.all(from c in Cause,
    left_join: ac in "artists_causes", on: c.id == ac.cause_id,
    left_join: a in Artist, on: a.id == ac.artist_id,
    preload: [:artists]
    )
  end

and got past the artists: #Ecto.Association.NotLoaded<association :artists is not loaded> problem. However, I am now getting the error FunctionClauseError at POST /graphiql\n\nException:\n\n ** (FunctionClauseError) no function clause matching in anonymous fn/3 in Absinthe.Resolution.Helpers.dataloader/1 This message maybe pointing towards something with the Absinthe.Resolution.Helpers.dataloader/1 method but it does not make sense to me as I have the helpers imported
@hlx please see below my Cause context

defmodule MyApp.Causes do

 import Ecto.Query, warn: false
 alias SingForNeeds.Repo

 alias MyApp.Causes.Cause
 alias MyApp.Artists.Artist


 def list_causes do
   Repo.all(from c in Cause,
   left_join: ac in "artists_causes", on: c.id == ac.cause_id,
   left_join: a in Artist, on: a.id == ac.artist_id,
   preload: [:artists]
   )
 end

Is there something else I could be missing. I will respond to you guys incase I make any progress.

I think it’s better if I setup a small example for you (tomorrow). In the mean time I suggest you read up on dataloader and when and why you should use it (the docs are better than my explanation).

Edit: typo

The query can be simplified to

‘from c in Cause, join: a in assoc(c, :artists), preload: [:artists]’

This way it uses the associations set buy the schema.

Edit: formatting because mobile

Thanks @hlx for the simplified query, I however want to do a left join so it can load causes with no artists as well atleast as artists:[], and hence I did update yours to ‘from c in Cause, left_join: a in assoc(c, :artists), preload: [:artists]. My query returns [] if I do inner join and the same error above when I do left join # FunctionClauseError at POST /graphiql\n\nException:\n\n ** (FunctionClauseError) no function clause matching in anonymous fn/3 in Absinthe.Resolution.Helpers.dataloader/1\n
Does your query run with left join as well?
Also, do you have a link where they are implementing the dataloader for M to M relationships? That would be helpful Unfortunately I havent been able to find good examples in this area. Thanks again!

It was a actually missing something, try this (I on my phone):

‘from c in Cause, left_join: a in assoc(c, :artists), preload: [artists: a]’

I still get the same error for left join (FunctionClauseError) no function clause matching in anonymous fn/3 in Absinthe.Resolution.Helpers.dataloader/1\n \n The following arguments were given to anonymous fn/3 and not with a right join.