What is Ecto.Multi?
Into the Ecto.Multi-verse
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} endend)|> 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!
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.