Hackernoon logoUsing Ecto.Multi for Complex Database Transactions by@QuantLayer

Using Ecto.Multi for Complex Database Transactions

QuantLayer Hacker Noon profile picture

@QuantLayerQuantLayer

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": "[email protected]",
"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:

“Ecto.Multi makes it possible to pack operations that should be performed together (in a single database transaction) and gives a way to introspect the queued operations without actually performing them. Each operation is given a name that is unique and will identify its result or will help to identify the place of failure in case it occurs.”

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 [email protected] — I would love to chat with you. Follow us on Twitter at https://twitter.com/@QuantLayer

Hacker Noon is how hackers start their afternoons. We’re a part of the @AMIfamily. We are now accepting submissions and happy to discuss advertising &sponsorship opportunities.
To learn more, read our about page, like/message us on Facebook, or simply, tweet/DM @HackerNoon.
If you enjoyed this story, we recommend reading our latest tech stories and trending tech stories. Until next time, don’t take the realities of the world for granted!

Tags

Join Hacker Noon

Create your free account to unlock your custom reading experience.