A brief guide to Ecto.Multi

One of the many exciting additions to Ecto 2.0, which was released sometime ago, was Ecto.Multi — a set of utilities aimed at composing and executing atomic operations, usually (but not always, as you’ll see below) performed against the database. Furthermore— it handles rollbacks, provides results on either success or error, flattens-out nested code and saves multiple round trips to the database. Basically you need some Ecto.Multi in your Elixir life.

Surprisingly, many people I’ve spoken to seem to have missed it and have no idea all this functionality exists. If you haven’t used Ecto.Multi — keep reading! If you have, then you might discover a trick or two.

Creating a Multi

Everything starts with a%Multi{} struct. Regardless of what you’re doing, you always have to provide a new or an existing Multi to most of the functions, which you can easily create by calling new() :

iex> Ecto.Multi.new()
%Ecto.Multi{names: #MapSet<[]>, operations: []}

Executing Multi operations

Easy — you just call Repo.transaction(multi) :

iex> Ecto.Multi.new() |> Repo.transaction()
{:ok, %{}}

Clearly we just ran an empty Multi, which was obviously successful since nothing was performed (and nothing returned in the second element of the {:ok, return} tuple. To make Multis useful, you need to add operations to it.

Working with individual changesets

The most common (and easy) scenario: multiple changesets. Instead of using the usual Repo.insert et al functions, you can use their Multi equivalent. They also accept an %Ecto.Changeset, so it is an easy change to group them into a single database transaction:

Ecto.Multi.new()
|> Ecto.Multi.insert(:team, team_changeset)
|> Ecto.Multi.update(:user, user_changeset)
|> Ecto.Multi.delete(:foo, foo_changeset)
|> Repo.transaction

The atoms used — :user ,:team and :foo—are chosen by you. You can pass anything (also you can use a string, instead of an atom) as long as it’s unique for the current Multi. The changeset variables are the usual%Ecto.Changeset structs you know and love.

Result of a previous operation

Operations will be run in the order they’re added to the Multi. Often you need the result of a previous operation, which you can get by running a custom Multi operation, like so:

Ecto.Multi.new()
|> Ecto.Multi.insert(:team, team_changeset)
|> Ecto.Multi.run(:user, fn %{team: team} ->
# Use the inserted team.
Repo.update(user_changeset)
end)

Ecto.Multi.run needs a name for its first parameter, just like Multi insert/delete/update etc, which I have called :user; the second is a function, which provides you with the results of previous operations. The results are just a map, and you can use the unique key to pattern-match and get the result for a specific operation, in this case :team.

Notice that here we call Repo.update — you need to return an {:ok, val} or a {:error, val} tuple from Multi.run. Using Repo.update will give us just that.

Custom operations

Actually, Multi.run could be used for pretty much anything. As long as you return a success/error tuple, it will become part of the same atomic transaction:

Ecto.Multi.new()
|> Ecto.Multi.insert_all(:users, MyApp.User, users)
|> Ecto.Multi.run(:pro_users, fn %{users: users} ->
result = Enum.filter(users, &(&1.role == "pro"))
{:ok, result}
end)

Here :pro_users will be available to use for subsequent operations and in the result returned by Repo.transaction. It’s a great way to ensure code is run together with the rest of the database operations. If the :users operation fails or something else, we’ll never get to filter them.

Working with multiple Multis and dynamic data

The beauty of Ecto.Multi is that it’s just a data structure, which you can pass around. It is easy to dynamically generate data and combine different multis together, before executing everything as a single transaction:

posts_multi = 
posts
|> Stream.filter(fn post ->
# Filter old posts...
end)
|> Stream.map(fn post ->
# Create changesets.
Ecto.Changeset.change(post, %{category: "new"})
end)
|> Stream.map(fn post_cs ->
# Create a Multi with a single update
# operation, generating a unique key for the op.
key = String.to_atom("post_#{post_cs.data.id})
Ecto.Multi.update(Ecto.Multi.new(), key, post_cs)
end)
|> Enum.reduce(Multi.new(), &Multi.append/2)

Using Multi.append/2 we now have a single Multi with all update operations in order. There’s also mergeand prepend.

Handling transaction results

Once you call Repo.transaction, you can pattern-match the result tuple.

In the case of success, you will receive all {:ok, result} with result being all operations and their successful results.

If it fails, all database operations will be rolled back, and you will be given {:error, failed_operation, failed_value, changes_so_far} which allows to handle errors from specific operations individually and inspect them. Note that changes_so_far simply means “operations that wen’t well until this one failed” and no data is actually left in the database.

Ecto.Multi.new()
|> Ecto.Multi.insert(:team, team_changeset)
|> Ecto.Multi.update(:user, user_changeset)
|> Ecto.Multi.delete(:foo, foo_changeset)
|> Repo.transaction
|> case do
{:ok, %{user: user, team: team, foo: foo}} ->
# Yay, success!
{:error, :foo, value, _} ->
# Tsk tsk, foo failed.
{:error, op, res, others} ->
# One of the others failed!
end

Final words

This brief guide covers most of the functions available, but as always, refer to the official documentation, which is excellent:

If you find this short guide useful and would like to see more content on Elixir, Ecto and web performance — please share and hit the 👏 button. Thanks!

I’m Svilen — a full-stack web developer and co-founder at Heresy. We’re always looking for engineers who enjoy working with the latest technologies and solving challenging problems. If you’re curious, check out jobs page!