How to add new assocs in put_assoc?

Hi,

I’m having hard time undestanding how put/cast -assocs work in many to many relationships.
What I have here is a user model which could be used both as “client” or as “owner”.

So a User can be an “owner” or a “client” or both.
Eg.
User 1 (owner of user 2)
User 2 (belongs to user 1 and 2)
User 3 (owner of user 2)

I’ve managed to sort of getting this to work without really undestanding why or how, but it works like shown here:

Here’s my schema for user:

defmodule User do
  schema "users" do
    many_to_many :clients, User, join_through: OwnerClient, join_keys: [owner_id: :id, client_id: :id], on_delete: :delete_all
    many_to_many :owners, User, join_through: ClientOwner, join_keys: [owner_id: :id, client_id: :id], on_delete: :delete_all
    field :email, :string
  end

  def changeset(%User{} = user, attrs) do
    user
    |> cast(attrs, [:email])
    |> unique_constraint(:email)
  end
end

And here’s what I found to be the only way to get this thing work with self-references, two seperate schemas:

defmodule ClientOwner do
  schema "client_owner" do
    belongs_to :owner, User
    belongs_to :client, User
  end
end

defmodule OwnerClient do
  schema "owner_client" do
    belongs_to :owner, User
    belongs_to :client, User
  end
end

Here are the migration files:

defmodule Repo.Migrations.CreateOwnerClient do
  def change do
    create table(:owner_client) do
      add :owner_id, references(:users, on_delete: :delete_all)
      add :client_id, references(:users, on_delete: :delete_all)
    end
    create unique_index(:owner_client, [:owner_id, :client_id], name: :uniq_owner_client)
  end
end

defmodule Repo.Migrations.CreateClientOwner do
  def change do
    create table(:client_owner) do
      add :owner_id, references(:users, on_delete: :delete_all)
      add :client_id, references(:users, on_delete: :delete_all)
    end
    create unique_index(:client_owner, [:owner_id, :client_id], name: :uniq_client_owner)
  end
end

Ok, there’s probably a better way to get this thing up but I couldn’t find any. So like says it works and here’s how I use it:

to Create a user:
User.changeset(%User{}, %{email: "whatever@sample.com"}) |> Repo.insert!

Given we have 3 users now, I do the following to create the assocs:

owner = Repo.get_by(User, id: 1) |> Repo.preload([:clients, :owners])
client = Repo.get_by(User, id: 2) |> Repo.preload([:owners, :clients]) |> Ecto.Changeset.change |> Ecto.Changeset.put_assoc(:owners, [owner]) |> Repo.update!
owner |> Ecto.Changeset.change |> Ecto.Changeset.put_assoc(:clients, [client]) |> Repo.update!

So, here I have a User 1 who owns User 2 who then again belongs to User 1

Nice!

But When I try to add a new ownership association it fails:

owner = Repo.get_by(User, id: 3) |> Repo.preload([:clients, :owners])
client = Repo.get_by(User, id: 2) |> Repo.preload([:owners, :clients]) |> Ecto.Changeset.change |> Ecto.Changeset.put_assoc(:owners, [owner]) |> Repo.update!

What happens next is an error:

** (RuntimeError) you are attempting to change relation :owners of
User but the `:on_replace` option of
this relation is set to `:raise`.

By default it is not possible to replace or delete embeds and
associations during `cast`. Therefore Ecto requires all existing
data to be given on update. Failing to do so results in this
error message.

If you want to replace data or automatically delete any data
not sent to `cast`, please set the appropriate `:on_replace`
option when defining the relation. The docs for `Ecto.Changeset`
covers the supported options in the "Related data" section.

However, if you don't want to allow data to be replaced or
deleted, only updated, make sure that:

  * If you are attempting to update an existing entry, you
    are including the entry primary key (ID) in the data.

  * If you have a relationship with many children, at least
    the same N children must be given on update.

What does this mean? As I don’t want to replace the existing assocs but merely to add a new one I guess the error message tries to tell me to add the existing users to put_assoc but that only seems to work if I put it as a single item like this:

... |> Ecto.Changeset.put_assoc(:owners, [old_owner, new_owner]) |> ...

but what if I already have multiple older owners? I can’t seem to be able to put a list of owners in the put_assoc

trying to put a list of owners there like this:

... |> Ecto.Changeset.put_assoc(:owners, owners) |> ...

gives me this:

** (Ecto.InvalidChangesetError) could not perform update because changeset is invalid.

Applied changes

    %{}

Params

    nil

Errors

    %{owners: [{"is invalid", [type: {:array, :map}]}]}

So what gives? how can I insert new assocs along with existing ones and does this ridiculous looking many-to-many-thingy even work in the long run or are there some hidden secrets somewhere describing a better way?

Cheers and love to anyone willing to go through this god awful “question”!

I never use associations, I just put each table in straight-out in the right order, which is really really necessary for many-to-many with data in the joins.

Not really sure I undestand these concepts you mentioned. How do you put tables in right order and what is data in the joins? But thanks anyways!

And for my original question I found a solution: I can indeed insert a list of oweners in put_assoc (don’t know what I did wrong the first time I tested it)