Using Ecto.Multi for Complex Database Transactions

QuantLayer
Nov 23, 2016 · 4 min read

by Vikram Ramakrishnan

Recently, we worked on a client project that required sending over a number of fields to the server during user registration. Some of these fields (email, password, etc.) were part of the user schema and others with other schema. Since these other schema depend on user, we would have to nest conditional transactions in our RegistrationController, which would provide potential for multiple points of failure. Rather than nest these conditional transactions, we wanted to be able to easily sequence our transactions and match on errors and failures. The following is an explanation of how we used Ecto.Multito make this easy.

Consider the following example. You have two schema: user and address. A user has_many addresses and an address belongs_to a user. During the user registration process, you want the user to submit their user details along with their mailing address details. For simplicity sake, let’s assume we’re validating on all of the fields, so in the event of any fields not being sent over to the server, the entire transaction fails. Here’s an example of good request params being sent over to the server:

{
"user": {
"email": "vikram@quantlayer.com",
"password": "password1",
"phone_number": "6176176176"
},
"address": {
"city": "Cambridge",
"country": "US",
"postal_code": "02139",
"state_province": "MA",
"street_line1": "5 QuantLayer Ave."
}
}

Since a mailing address belongs to a user, we have to create a user to associate with the address before the address can be created. Keeping that all in mind, the logic might look something like this:

1. Try creating a user
2. If user creation fails, return an error
3. If user creation succeeds, try creating an address
4. If address creation fails, delete the user and return an error
5. If address creation succeeds, return the user and jwt

Here’s an example of what this looks like in the controller:

user_changeset = User.changeset(%User{}, user_params)case Repo.insert(user_changeset) do {:ok, user} ->
address_changeset =
%Address{user_id: user.id}
|> Address.changeset(address_params)
case Repo.insert(address_changeset) do
{:ok, _address} ->
{:ok, jwt, _full_claims} =
Guardian.encode_and_sign(user, :token)
conn
|> put_status(:created)
|> render(MyApp.SessionView, "create.json", jwt: jwt, user: user)
{:error, changeset} ->
Repo.delete(user)
conn
|> put_status(:unprocessable_entity)
|> render(MyApp.RegView, "error.json", changeset: changeset)
end
{:error, changeset} -> conn
|> put_status(:unprocessable_entity)
|> render(MyApp.RegView, "error.json", changeset: changeset)
end

There are a few things I don’t like about this. First of all, the nested case statements make it difficult to follow. Secondly, we’re deleting the newly created user on an address failure, which increases the number of database transactions. And finally, we aren’t handling errors based on bad inputs for both user and address params. This approach is really flimsy. Imagine adding another step, like required credit card details. Nesting further case statements along with tracking multiple points of error become a hassle. I would rather be able to rollback the entire transaction if any part of it fails.

Enter Ecto.Multi

Ecto.Multi lets us handle multiple, dependent Repo transactions.

The docs (https://hexdocs.pm/ecto/Ecto.Multi.html) describe it as follows:

So, let’s rewrite the example above with Ecto.Multi:

user_changeset = User.changeset(%User{}, user_params)multi =
Multi.new
|> Multi.insert(:user, user_changeset)
|> Multi.run(:address, fn %{user: user} ->
address_changeset =
%Address{user_id: user.id}
|> Address.changeset(address_params)
Repo.insert(address_changeset)
end)
case Repo.transaction(multi) do
{:ok, result} ->
{:ok, jwt, _full_claims} =
Guardian.encode_and_sign(result.user, :token)
conn
|> put_status(:created)
|> render(MyApp.SessionView, "create.json", jwt: jwt, user: result.user)
{:error, :user, changeset, %{}} ->
conn
|> put_status(:unprocessable_entity)
|> render(MyApp.RegView, "error.json", changeset: changeset)
{:error, :address, changeset, %{}} ->
conn
|> put_status(:unprocessable_entity)
|> render(MyApp.RegView, "error.json", changeset: changeset)
end

Here, we assign an Ecto.Multi.new transaction to multi. Multi accepts changesets through functions like insert. Note that the :user and :address are the unique names we assign to the operations in Multi.insert/2 and Multi.run/2, which is why we can pass user to Multi.run/2. The changesets are checked, and if there are errors, the transaction doesn’t start and returns the errors. We then use Multi.run to pass an arbitrary function, which is dependent on the user in the line prior. When we execute the transaction with Repo.transaction(multi), we can pattern match on all the possible outcomes, which makes adding more requirements later on easier.

More good perspective on the purpose of the library is contained here in the original Ecto.Multi PR: https://github.com/elixir-ecto/ecto/issues/1114

Interested in discussing custom software needs more broadly? Drop me a line at vikram@quantlayer.com — I would love to chat with you. Follow us on Twitter at https://twitter.com/@QuantLayer

HackerNoon.com

#BlackLivesMatter

Sign up for Get Better Tech Emails via HackerNoon.com

By HackerNoon.com

how hackers start their afternoons. the real shit is on hackernoon.com. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

QuantLayer

Written by

https://quantlayer.com

HackerNoon.com

Elijah McClain, George Floyd, Eric Garner, Breonna Taylor, Ahmaud Arbery, Michael Brown, Oscar Grant, Atatiana Jefferson, Tamir Rice, Bettie Jones, Botham Jean

QuantLayer

Written by

https://quantlayer.com

HackerNoon.com

Elijah McClain, George Floyd, Eric Garner, Breonna Taylor, Ahmaud Arbery, Michael Brown, Oscar Grant, Atatiana Jefferson, Tamir Rice, Bettie Jones, Botham Jean

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store