I recently set out to implement user registration for a project I’m working on
in Elixir/Phoenix. It wasn’t long before I encountered a challenge that I have
stumbled upon with every other ORM library: accepting a collection of form
inputs and saving it across multiple (related) records in the database.

There’s more than one way to tackle the problem (with varying degrees of
elegance), but I discovered that Ecto
lends itself particularly well to solving this problem once you are familiar
with tools available.

The registration form

Let’s assume that you have a users table and a teams table, and upon
submitting the registration form you need to create a new team record and a
new user record (the team owner).

At a minimum, our form needs to collect the following fields:

Team name

Email address

Password

Since this HTML form includes fields that belong in multiple database records,
it does not make sense to bind the form directly to the User or Team changesets.

Upon submission, the form data is available in the request params under the
"signup" key. This may be suitable for very simple use cases, but quickly becomes
cumbersome when you need more complicated logic, like data validations and
default values.

Fortunately, Ecto changesets do not have to correspond to an actual database table!
This means we can still use a changeset to implement our validation logic
in a “virtual” model. Let’s call it Registration and drop it in our models directory:

# web/models/registration.exdefmoduleMyApp.RegistrationdoimportEcto.Changeset@types%{team_name::string,email::string,password::string}@doc"""
Builds a changeset based on the `struct` and `params`.
"""defform_changeset(struct,params\\%{})do{struct,@types}|>cast(params,Map.keys(@types))|>validate_required([:team_name,:email,:password])|>validate_length(:team_name,min:1,max:255)|>validate_length(:email,min:1,max:254)|>validate_length(:password,min:6)endend

In the create action, we check to see if the validations pass; if not,
then we re-render the form (with errors). The %{changeset | action: :insert}
step is important, because it signifies to the form helper that errors should
be rendered.

The template looks essentially the same as the first example, except the
first argument is @changeset instead of @conn:

Persisting the data

The persistence phase should go something like this:

Insert a record in teams

Insert a record in users (with a foreign key pointing to the team)

In the event either operation fails, rollback all inserts

This is a perfect candidate for a database transaction because we
want to guarantee rollback on failure. Conveniently, Ecto comes with a handy module called Ecto.Multi
that facilitates grouping a pipeline of database operations for transactions.

Let’s build on our Registration module by adding a operation function, and
add registration_changeset functions to the User and Team models. (One of the steps not
implemented in this example is the put_password_hash function in the User
module which is responsible for transforming the raw password into a hashed one
for storage).

The Registration.operation function is responsible for building an Ecto.Multi
structure that can be passed to the Repo.transaction
function. The first step inserts the Team record, and the second step
receives the newly-created Team and associates the User to it when inserting.

Now that we have an function for generating our operation, we can utilize it
in our controller.