Associating Two Existing Many-to-Many Records with Ecto
This article is one of many efforts to seed Google with common sense solutions to problems that a first time Elixir/Phoenix developer might thrash against.
I recently became very frustrated during an attempt to complete a seemingly simple task using Ecto in a Phoenix application. I had two existing records in a many-to-many relationship that I wanted to associate (i.e., create the join table record for.) I was getting very confused by the different use cases for
Ecto.Changeset.put_assoc/4, and I want to share my solution so I can (a) help other folks that may be blocked but also (b) get feedback on a better way to accomplish this if I misunderstood how to do it.
Before I get into the code, let’s discuss a simplified version of my schema: I have
companies that are in a many-to-many relationship with each other through a
Now to my problem; I need to add an existing user to an existing company. Via my API I’ve been given a
user_id and a
company_id, from which I've fetched and loaded a
user struct and a
My first instinct was to use
Ecto.Changeset.put_assoc to put the
company into the list of the
user's companies. The issue there, however, was that
put_assoc expects to be passed the entire list of associated items at which point the
:on_replace policy that's configured on the association will be consulted as to what to do with any existing records in the association that are not in the supplied list.
When adding one more item to a
has_many association it's suggested to use
Ecto.build_assoc/3 (doc) but wasn't going to work for me because I'm not creating and associating a new record, but instead associating two existing records.
Finally, I moved on to considering
Ecto.Changeset.cast_assoc/3. The issue there was that it's designed to handle the management of the association as a whole. From the documentation:
cast_assoc/3 is useful when the associated data is managed alongside the parent struct, all at once.
To work with a single element of an association, other functions are more appropriate. For example to insert a single associated struct for a has_many association it’s much easier to construct the associated struct with Ecto.build_assoc/3 and persist it directly with Ecto.Repo.insert/2.
Furthermore, if each side of the association is managed separately, it is preferable to use put_assoc/3 and directly instruct Ecto how the association should look like.
In my case I don’t have access to the entire association via the
params- I only know about this one
user that I want to associate with this one
My solution was to use
put_assoc/4, but wrap all of the existing items in the association in an
Ecto.Changeset in order to preserve the unchanged
companies in the association while creating the new association to the desired
It’s still not clear to me that this is the very best way to accomplish the task of associating two existing many-to-many-associated records, but if you know ways it can be done better please respond in comments!