What is Ecto.Multi?

Into the Ecto.Multi-verse

Marvel: “Infinity War is the most ambitious crossover event in history.” Flatiron Labs:

If you’re gazing across the Elixir landscape and looking for a way to execute operations only if other operations succeed while minimizing the number of transactions, look no further! Ecto.Multi could be just what you’re looking for.

At the core of it, Ecto.Multi is a struct, which means it can be passed around like any other struct in Elixir. It’s most commonly used to bundle a series of database operations together. If anything in the series fails, all of the previous database operations in the multi are never applied. This is great because it means no bad data ever makes it to the database and no stale data gets left behind.

While database operations are the most common use case for Ecto.Multi, they’re certainly not the only thing Ecto.Multi is able to handle. In fact, Ecto.Multi.run can be used to execute any function, which is why Ecto.Multi is so powerful. Every operation in a multi is given a unique name, and that name can be used by any operation to access the results of all previous operations. So with Ecto.Multi, you can access all data that’s already been executed in the sequence and also run any function. Let’s say we want to update accounts, but for security reasons we will only allow the account to be updated if the update is successfully tracked both internally and externally. That could look something like this.

alias Ecto.Multi

Multi.new()
|> Multi.update(:update_account, account_changeset)
|> Multi.insert(:insert_event, event_changeset)
|> Multi.run(:track_event, fn _repo, %{insert_event: event} ->
  send_event_to_external_tracker(event)
end)
|> Repo.transaction() # actual execution

Basically we’ve updated an account, inserted an event, and sent that event to an external, third-party tracking service. Let’s talk a little bit about the anonymous function being passed to run since a couple notable things are happening. The event being used by send_event_to_external_tracker comes from pattern matching on previous results (includes update_account and insert_event) in the anonymous function’s head. Also, every function that’s passed to run is required to return either an {:ok, value} or {:error, value} tuple like all the Repo functions do.

Those tuples are really important. The success tuple is what notifies the multi to continue onto the next operation. The error tuple notifies the multi to stop executing and return without letting anything make it to the database. If, for whatever reason, send_event_to_external_tracker were to error out, the database would remain pristine and neither the update nor the insert would have impacted it. There’s a huge benefit to that clean slate. Keeping external and internal data in sync can be a time-consuming, error-prone, and tedious process. With Ecto.Multi, we can get it for free without having to manage the process ourselves.

For our above multi we would have results that look something like this once Repo.transaction() is finished.

{:ok, %{update_account: value, insert_event: value, track_event: value}}
# or if the insert operation had failed
{:error, :insert_event, changeset, %{update_account: value}}
# or if the run operation had failed
{:error, :track_event, error, %{update_account: value, insert_event: value}}

The {:ok, value} tuple gives access to every successfully returned value for every step of the multi and they’re all accessible by the unique name given to them. When the multi returns an error, we have access to the error and also the values from the previous operations that were successful even if they were never applied.

Some interesting tidbits about Ecto.Multi

In the above case, we were trying to keep external and internal data in sync. But let’s say we don’t actually need everything to stay in sync. What if instead we wanted to bulk update the external service using our events table? We could do it intermittently after the fact should the track_event operation fail. The code below would allow us to do that.

Multi.new()
|> Multi.update(:update_account, account_changeset)
|> Multi.insert(:insert_event, event_changeset)
|> Multi.run(:track_event, fn _repo, %{insert_event: event} ->
  result = send_event_to_external_tracker(event)
  case result do
    {:ok, value} -> {:ok, value}
    {:error, error} ->
      notify_about_error(error)
      {:ok, error}
  end
end)
|> Repo.transaction() # actual execution

Ecto.Multi actually has the flexibility to allow us to customize behavior by manipulating the return value from run. Here in the run function, we’re always returning an {:ok, value} tuple, so it never causes execution to stop, and the previous operations will still be applied. We’re also able to have the error end up in the results map if we wanted to when everything is said and done.

Something else interesting to note is that if the function passed to run contains a database operation (ie. insert, update, delete), it will also not get applied along with the explicitly called database operations. It can be helpful to use Ecto.Multi.run in conjunction with Ecto.Repo.insert/update because run makes the data from previous parts of the multi easily accessible, and sometimes your insert or update depends on that information.

Ecto.Multi is an interesting tool in the Elixir ecosystem and can be applied to many use cases. For more information, check out the documentation. Or go straight for the source code.


Want to explore the mysteries of Elixir with a bunch of awesome people? We’re hiring!


Footer top

To learn more about Flatiron School, visit the website, follow us on Facebook and Twitter, and visit us at upcoming events near you.

Flatiron School is a proud member of the WeWork family. Check out our sister technology blogs WeWork Technology and Making Meetup.

Footer bottom