Oh I think in this case, it’s exact case noted in put_assoc
, where it is recommended you use something other than put_assoc
: https://hexdocs.pm/ecto/Ecto.Changeset.html#put_assoc/4-example-adding-a-comment-to-a-post
I was facing your exact problem - in my case adding new address for a user.
Eventually maybe I’ll add address checks to find out same address was already created - however for now, my plan is to always add new addresses.
So that follows your example - I have users, addresses, and users_addresses as tables.
After working many strategies, I think the best case would be to be more explicit than be implicit in this case, which follows along the examples listed in the link in ways to insert a single new entry. Note that the examples in the link is not using a many-to-many table.
In my case, I decided to explicitly do the actual DB operations wrapped in Ecto.Multi
. I think this is a case where cast_assoc
and put_assoc
can’t help, and I don’t think there are any helper functions that deal with adding extra columns in the many-to-many table. So, my strategy was if I at least be slightly lower level and explicitly drive the DB operations, at least it will be more maintainable later on.
Beauty of Elixir is that… well lemme show you the code first:
profile = Repo.one(Profile)
my_role = "some role"
result =
Ecto.Multi.new()
|> Ecto.Multi.insert(:user, User.changeset(%User{}, address))
|> Ecto.Multi.insert(:user_profile, fn %{user: user} ->
%UserProfile{user_id: user.id, profile_id: profile.id, role: my_role}
end)
|> Repo.transaction()
case result do
{:ok, %{user_profile: user_profile}} ->
{:ok, user_profile}
{:error, _failed_operation, failed_value, _changes_so_far} ->
{:error, failed_value}
end
These few lines chain insertions, and also do transaction rollback. Amazing! 
I’m still learning Ecto so there’s probably better ways to do it - but I tend to rather be close to the DB (as Ecto is still new for me)