How to implement a m:n-Relation (many-to-many)

Hello,

it is frustrating. Excuse me, but I try to implement a many-to-many Relation without a result. What I want is: I have Users and I have groups. Between Users and Groups exists a m:n-Relation. So I implement:

A Group:

defmodule Lmsphx.Accounts.Group do
  use Ecto.Schema
  import Ecto.Changeset
  import Ecto.Query
  alias Lmsphx.Accounts

  schema "group" do
    field :title, :string
    #has_many :user, Accounts.User

    many_to_many(:user, Accounts.User, join_through: Lmsphx.Accounts.UserGroup)

    timestamps()
  end

  @doc false
  def changeset(group, attrs) do
    group
    |> cast(attrs, [:title])
    |> validate_required([:title])
  end

  def get_groups(nil), do: []

  def get_groups(ids) do
    Repo.all(from a in Accounts.Group, where: a.id in ^ids)
  end
end

A User:

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

  schema "user" do
    field :email, :string
    field :firstname, :string
    field :gebdate, :date
    field :lastname, :string
    field :login, :string
    field :password, :string

    many_to_many(:group, Accounts.Group, join_through: Lmsphx.Accounts.UserGroup)

    timestamps()
  end

  @doc false
  def changeset(user, attrs) do
    IO.write("Get user data ...")
    user
    |> cast(attrs, [:lastname, :firstname, :login, :password, :email, :gebdate])
    |> validate_required([:lastname, :firstname, :login, :password, :email, :gebdate])
    |> cast_assoc(:group, required: true)
  end
end

And an UserGroup:

defmodule Lmsphx.Accounts.UserGroup do
  use Ecto.Schema
  alias Lmsphx.Accounts

  import Ecto.Changeset

  @primary_key false
  schema "user_group" do
    belongs_to :user, Accounts.User
    belongs_to :group, Accounts.Group
  end

  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:user_id, :group_id])
    |> validate_required([:user_id, :group_id])
  end

end

In a LiveView I want to generate Checkboxes, so I can put a User to different groups. I wrote a multiselect_checkboxes-Function, which I want to use in this liveview. But If I call this function with:

multiselect_checkboxes(
        f,
        :group_collection,
        Enum.map(@group_collection, fn a -> { a.title, a.id } end),
        selected: Enum.map(@changeset.data.group,&(&1.id))
      ) 

I got the error: ** protocol Enumerable not implemented for #Ecto.Association.NotLoaded of type Ecto.Association.NotLoaded (a struct)**

It makes sense, BUT!, where I have to implement the preload?

regards,
Sven

There are some points I would not do like You do…

many_to_many(:user, Accounts.User, join_through: Lmsphx.Accounts.UserGroup)

I would use plural, and the name of the join table, not a struct.

many_to_many(:users, Accounts.User, join_through: "groups_users")

No need to have an intermediate struct if there is no custom attributes on it.

The changeset.data is the user. Did You preload groups?

I usually do this when calling context functions create/update

kokolegorille,

Thank you very much for your answer.
No I did not preload, because I do not know, where to implement this. In this user.eex (see above), Repo.preload did not work. Where are your create/update - Functions situated???

regards,
Sven

I do something like this…

changeset = Core.event_upcoming_changeset(preload_associations(%Event{}))


  defp preload_associations(any) do
    Core.preload(any, [
      :creator,
      :department,
      :categories,
      :groups,
      :language,
      :documents,
      :annotations,
      :readers
    ])
  end


defdelegate preload(any, associations), to: Repo

kokolegorille,

OK. I see you have written a private function. I have besides the file user.ex, group.ex and user_group.ex a file called accounts.ex. In accounts.ex on was generated via mix, helper functions for user and group. This file looks like (excerpt):

def update_user(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> Repo.update()
  end

  def delete_user(%User{} = user) do
    Repo.delete(user)
  end

  def change_user(%User{} = user, attrs \\ %{}) do
    User.changeset(user, attrs)
  end

I tried to preload here in this file. But error stayed. :frowning:

regards,
Sven

It’s difficult to say without code…

There should be the module and line number in the fullstack error.

Dear kokolegorille,

I think, and thats my main problem, it would help me, when I could find out, how to debug my app. I knew the structures of these data, I could imagine, how to realize this. For instance, I do not know for instance, how the changeset does look like. How can I look into this with help of iex?

regards,
Sven

If in a template…

<%= inspect @changeset %>

… or in the function

  def update_user(%User{} = user, attrs) do
    user
    |> User.changeset(attrs)
    |> IO.inspect(label: "Yo!")
    |> Repo.update()
  end

Because IO.inspect returns the input value, You can use it easily in any pipes without breaking your code.

kokolegorille,

thank you so much!!! I think I found it out. I extend my function change_user. It looks now like:

def change_user(%User{} = user, attrs \\ %{}) do
    IO.write("Create a new User ...")
    user = Repo.preload(user, :groups)
    User.changeset(user, attrs)
  end

In other words, I add the line user = Repo.preload(user, :groups) and it works.

Thank you very much!

regards,
Sven

Nice…

In some case, I use this pattern.

  def ensure_preload(any, assoc) do
    case Ecto.assoc_loaded?(Map.get(any, assoc)) do
      true -> any
      false -> any |> Repo.preload(assoc)
    end
  end

Thanks a lot!!! I wish you a wonderful evening!!! Now I can inspect all I want and understand now, where the mistakes where.

Your function ensure_preload is beautyful, thank you for your hint!

regards,
Sven

This isn’t necessary. Repo.preload is documented to be a no-op if the association is already loaded.

In case the association was already loaded, preload won’t attempt to reload it.

You can use the force: true option if you wish to re-fetch

2 Likes

I have this in my app too. I called that table group_memberships and the schema MyApp.Groups.Membership.