Building Many-to-Many Associations with cast_assoc and Nested Forms in Phoenix and Ecto

Mohamad El-Husseini
3 min readNov 18, 2016

--

A common requirement amongst web applications is the ability to create multiple database records in a single transaction. For example, a single form may be used to create a user, an organisation, and link them together with a membership record.

There are several ways to build this functionality, and Phoenix and Ecto make it easy with tools like Ecto.Multi and schemaless changesets, which I’ll discuss in later posts.

Update: You can now read how I implemented this functionality using Ecto.Multi and embedded schemas.

In this post, however, I want to look at the most common approach, using nested forms. Let’s start with a typical set of models.

Our requirement is to save a User, an Account, and a Membership record in one form submission. To keep things simple, the Account has a name column and the User an email column; both columns are required. We have a unique constraint on the email column. Although not shown here, there are foreign key references on the user_id and account_id columns in the memberships table to ensure referential integrity. Eventually, we will add a role column to the Membership schema.

If you come from Rails, you might use fields_for to implement nested forms and accepts_nested_attributes_for to map incoming params to your models. In Phoenix, we use changesets and inputs_for to the same effect.

Our API should support the following params structure.

In our controller, we need a new action that assigns a changeset with an identical structure.

Using this changeset, we can build our form.

We also need a create action in the controller to handle the form submission.

We’re almost done. In order for this to work, we need to update our Account and Membership changeset functions and instruct them to cast any nested associations in the params structure. We can do this using the cast_assoc/3 function in Ecto.

It’s a small but important change. cast_assoc/3 uses the first argument to retrieve the association params from the params struct, and forwards them to the changeset function of that association, which repeats the process, triggering validations and casting any nested associations.

Adding Roles

Let’s enhance our feature with a role column on the Membership schema.

Our new requirement is to have Membership records inserted with a role of admin at the time of registration. We can do this in a number of ways.

  1. Update incoming params in the controller to insert the role.
  2. Create an association for each role.
  3. Update the membership changeset to include the role after validation.

The first solution is a messy hack. The second adds complexity and isn’t scalable: creating one association for each role doesn’t scale. Let’s go with the third solution.

We added a put_role/1 function to that adds the admin role if the Membership changeset is valid.

Conclusion

Phoenix and Ecto make working with nested forms easy and painless. Their implementation is elegant and explicit — a refreshing change from working with accepts_nested_attributes_for.

That said, I don’t think nested forms are the best approach in this case. We are piping around nested associations to three different, registration-specific changesets, and the code feels fragmented. Also, in my opinion, the role requirement exposes the limitations of nested forms.

I don’t usually recommend nested forms for other reasons I’ll outline in the next post. But they are useful in many situations and are especially worth a look in Phoenix and Ecto.

In the next post we’ll look at refactoring our feature to use schemas not backed by database tables.

--

--

Mohamad El-Husseini

An entrepreneur, self-taught software engineer, and world traveller.