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”!