Elixir

Advanced Ecto.Multi Usage

I've been pushing Ecto.Multis further and further recently, trying to see what I can get out of them. Every time I push further, they continually surprise me with how much they are able to let you set database actions in a pipeline. I'd like to show off some advanced usages of Multis to show what you can do with them.

Using Ecto.Multi.run

Ecto.Multi.run is a nice way of chaining together functions that should happen as long as previous parts of the pipeline succeeded.

A common usage of run is sending email after an insert. This can be accomplished in other ways, but it is a lot cleaner with a pipeline than the nested cases I was using previously.

Just remember to return the correct shape inside the run! The value needs to be a tagged tuple, {:ok, value} or {:error, value}.

Ecto.Multi.new()
|> Ecto.Multi.insert(:order, Order.create_changeset(params))
|> Ecto.Multi.run(:email, fn _repo, %{order: order} ->
  order
  |> Emails.new_order()
  |> Mailer.deliver_later()
end)

Ecto.Multi.[insert|update] as a function

Another easy way to start using Multis is passing Ecto.Multi.insert or Ecto.Multi.update a function to generate a changeset.

In the simple case, you can chain together a series of inserts. When the inserts further down the pipeline require information from the previous inserts, you can use a function to get the current set of changes. Below is an example of creating a role for a user after creating the user.

Ecto.Multi.new()
|> Ecto.Multi.insert(:user, User.create_changeset(params))
|> Ecto.Multi.insert(:role, fn %{user: user} ->
  Role.create_changeset(user)
end)

We can also flip the run example above on its head and use run first before an insert to load a remote resource, and only attempt to insert the resource if it was successfully loaded.

Ecto.Multi.new()
|> Ecto.Multi.run(:fetch, fn _repo, _changes ->
  # This returns {:ok, order} | {:error, reason}
  Orders.get(remote_id)
end)
|> Ecto.Multi.insert(:local_order, fn %{fetch: order} ->
  Order.create_changeset(order)
end)

Enum.reduce those Ecto.Multis

If you have a list of items you wish to insert/update, then you can easily reduce across them with a multi as the accumulator. By reducing over the list accumulating into the multi, we keep each insert/update as a separate step in the multi pipeline. By being separate each step can fail on their own and return nicely.

The main thing I want to point out in this example is using a separate function to handle the Enum.reduce because Enum.reduce takes the enumerable as the first element, so you can't just pipe down a multi as normal. So instead we push that off into a private function to handle the inversion for us.

Ecto.Multi.new()
|> do_something(orders)
|> Repo.transaction()

# ...

defp do_something(multi, orders) do
  Enum.reduce(orders, multi, fn order, multi ->
    Ecto.Multi.update(multi, {:order, order.id}, Order.update_changeset(order))
  end)
end

In case you haven’t seen it before, you can add arbitrary functions in a Multi pipeline as long as the function returns the Multi being worked on.

Ecto.Multi.merge

Ecto.Multi.merge is something I learned fairly recently and it's an excellent tool to keep in your back pocket. This is something you won't need very often but the times you do need it, it will be very useful indeed!

To continue with a similar example above, you can fetch a remote resource, and then insert a local row for each item in an order. A merge is useful here because we want to keep each insert as a separate step of the multi while acting on unknown data (the amount of items on the order is only known after loading it).

Ecto.Multi.new()
|> Ecto.Multi.run(:fetch, fn _repo, _changes ->
  Orders.get(remote_id)
end)
|> Ecto.Multi.merge(:cache_items, fn %{fetch: order} ->
  Enum.reduce(order.items, Ecto.Multi.new(), fn item, multi ->
    Ecto.Multi.insert(multi, {:item, item.id}, Item.create_changeset(item)
  end)
end)

Conclusion

If you want to see how far you can push a Multi, please see this example from ExVenture. This multi lets you record changes from a valid changeset as separate change rows; staging them before finally committing later on in another Multi!

You've successfully subscribed to SmartLogic Blog
Great! Next, complete checkout for full access to SmartLogic Blog
Welcome back! You've successfully signed in.
Unable to sign you in. Please try again.
Success! Your account is fully activated, you now have access to all content.
Error! Stripe checkout failed.
Success! Your billing info is updated.
Error! Billing info update failed.