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.cast_assoc/3 and 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 users and companies that are in a many-to-many relationship with each other through a users_companies table.

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 company struct.

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 company.

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 company.

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!

Zachery Moneypenny is a Principal Developer at Rubyist for a long time, with experience in Golang and Javascript as well but moving towards Elixir and loving it.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.