Ecto.Association.NotLoaded with many to many relationship

I’m trying to add associations. The simplified example has users and categories, with an n-to-n relationship between them. I’m trying to add an existing category to a user in the map_user_categories function. But the result is always a Ecto.Association.NotLoaded error.

iex(37)> UserCategory.map_user_categories(u, c)
** (ArgumentError) schema M2mTest.Accounts.User does not have association or embed :user_categories
    (elixir 1.17.3) lib/enum.ex:2531: Enum."-reduce/3-lists^foldl/2-0-"/3
    (m2m_test 0.1.0) lib/m2m_test/accounts/user_category.ex:34: M2mTest.Accounts.UserCategory.map_user_categories/2
    iex:37: (file)

#user.ex

defmodule M2mTest.Accounts.User do
  use Ecto.Schema
  import Ecto.Changeset

  alias M2mTest.Accounts.Category
  alias M2mTest.Accounts.UserCategory

  schema "users" do
    field :name, :string
    field :email, :string

    many_to_many :categories, Category,
      join_through: UserCategory,
      join_keys: [user_id: :id, category_id: :id]

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(user, attrs) do
    user
    |> cast(attrs, [:name, :email])
    |> validate_required([:name, :email])
    |> unique_constraint(:email)
  end
end
#category.ex

defmodule M2mTest.Accounts.Category do
  use Ecto.Schema
  import Ecto.Changeset

  alias M2mTest.Accounts.User
  alias M2mTest.Accounts.UserCategory

  schema "categories" do
    field :name, :string

    many_to_many :users, User,
      join_through: UserCategory,
      join_keys: [category_id: :id, user_id: :id]

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(category, attrs) do
    category
    |> cast(attrs, [:name])
    |> validate_required([:name])
  end
end
#user_category.ex

defmodule M2mTest.Accounts.UserCategory do
  use Ecto.Schema
  import Ecto.Changeset

  alias M2mTest.Accounts.User
  alias M2mTest.Accounts.Category

  @primary_key false
  schema "user_categories" do

    belongs_to :user, User, primary_key: true
    belongs_to :category, Category, primary_key: true

    timestamps(type: :utc_datetime)
  end

  @doc false
  def changeset(user_category, attrs) do
    user_category
    |> cast(attrs, [:user_id, :category_id])
    |> validate_required([])
  end

  def create_user_category(user_id, category_id) do
    %__MODULE__{}
    |> changeset(%{user_id: user_id, category_id: category_id})
    |> M2mTest.Repo.insert()
  end

  def map_user_categories(user, categories) when is_list(categories) do
    user_categories_existing = user.categories || []

    user
    |> M2mTest.Repo.preload(:categories)
    |> Ecto.Changeset.change()
    |> Ecto.Changeset.put_assoc(:categories, user_categories_existing ++ categories)
    |> M2mTest.Repo.update()
  end

  def map_user_categories(user, category) do
    map_user_categories(user, [category])
  end
end

Since you don’t have :user_categories field in the files you showed, I would advise you to go search for who and where is trying to reference it and change it to :categories. That’s what I would do immediately without doing a further analysis first.

The preload/3 function does indeed preload the assoc, but notice that you retrieve user.categories before preloading them. That’s why you get an error, because you cannot concatenate the NotLoaded struct with a list.

I think you also copy/pasted the wrong error, which is what has thrown @dimitarvp off :slight_smile:

Hey, I already admitted I put minimal effort in my response, you got nothing on me. :smiley:

But yeah, looking at it, it became a XY problem indeed.

2 Likes