Run Transactions and Capturing Errors with Ecto.Multi
Explore how to use Ecto.Multi to group multiple database operations in Elixir, ensuring atomic transactions. Learn to manage operation results, handle errors with detailed feedback, and understand rollback behavior to write cleaner and safer database code.
The Ecto.Multi struct
The other way to use Repo.transaction is to pass in an Ecto.Multi struct rather than a function. The Ecto.Multi struct allows us to group our database operations into a data structure. When handed to the transaction function, the Multi’s operations run in order. If any of them fail, all of the others are rolled back.
Let’s take a look at an earlier example where we ran a transaction with an anonymous function.
Here’s how we can rewrite it using Multi.
There’s a lot here, so let’s walk through it.
A walkthrough
-
We created a new
Multiwith thenewfunction. The Ecto team recommends using this approach rather than trying to create the struct directly—don’t try to write something likemulti = %Multi{}. The exact structure ofEcto.Multiis subject to future change. When we callnew, it ensures that the struct will be properly initialized to us. If we create the struct directly, we’re on our own. -
We then add the two insert operations by piping the
Multiinto theinsertfunction. TheEcto.Multimodule has several functions that mirror the database operation functions in Repo, such asinsert,update,delete, and so on. Each of the operations that we add to theMultimust have a unique name, which is what the:artistand:logatoms are for. After that, we pass exactly what we would pass to theRepo.insertfunction—anArtiststruct for the first call, and ourLogchangeset for the second.
We don’t have any other options we need to include in our insert calls, but we could add them here if we did. The functions in Multi can accept the same options as their counterparts in Repo, so anything you might send to Repo.insert can be sent to Multi.insert as well.
At this point, we still haven’t touched the database. We have a list of operations stored in the Multi struct. When we finally pass the struct to Repo.transaction, the database begins executing the operations queued in the Multi. The return value, however, is different than what we get when we pass in a function:
The transaction succeeded, so we got a tuple with :ok and a map. The keys in the map are the unique names we provided to each operation in the Multi (:artist and :log in in this case). The values are the return values for each of those operations. This makes it easy for us to grab the return values of any or all of our operations. In this case, both of the operations were inserts, so we get structs representing our newly inserted records.
Capture errors with multi
Here’s where the two approaches diverge. If an error occurs in a Multi, we get detailed information on where the error occurred and what happened just before. Let’s take a look.
Examine the return value
To see this in action, let’s create a new Multi that performs an update on the Artist record we just inserted, then tries to insert an invalid changeset.
This time, the Multi failed, so we got a tuple with four items—the :error atom, the name of the operation that failed (:invalid), the value that caused the failure (in this case, the invalid changeset, with a populated errors field), and a map containing the changes so far. The database will have already rolled back these changes, but Ecto provides them for us to inspect if needed.
The benefit of this arrangement is that this single return value tells if we succeeded or, if we failed, exactly where we failed. This means that we can use pattern matching to respond to each of the success or failure scenarios separately.
That’s a lot cleaner than what we had when we were using anonymous functions with Repo.transaction. Here, we used a single case statement as our responses were fairly short. However, we could also use pattern-matched functions if we need more complex responses.
Examine the list of changes so far
The last value of the returned tuple is supposed to be a list of changes that occurred before the error happened. Let’s take another look at what we got in the last example.
We got an empty map, which seems surprising. The return value told us that the second operation in the Multi failed, so we would expect to see the result of the first operation in the list of changes so far.
This is because Ecto doesn’t like to waste the database’s time. If the
Multicontains operations that use changesets, Ecto first checks to ensure all the changesets are valid. If any are not, Ecto won’t bother running the transaction. It flags the invalid changeset and sends it back to us in the return value. There’s no need to trouble the database with an invalid changeset.
A different example
Let’s try a different example to see something besides an empty map. We’ll create a new Multi that starts with a successful update. We’ll then force an error by trying to insert a new %Genre{} record with a name that already exists in the database (as you might recall from here, the genres table has a unique index on the name column).
Now we can get a good look at that last value. The keys in the map correspond to our named Multi functions that have already been run. In this example, we just had the one :artist update, so that’s all this map contains. The value of the item is the result of the operation. Here we can see that our “Johnny Hodges” record was updated to “John Cornelius Hodges” as expected. Since the Multi failed (due to the addition of our bad_genre operation), though, the database rolled back the change. We can confirm that by looking at the database again.
We don’t get any records back when we search for “John Cornelius Hodges,” which confirms that our update was rolled back.