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.Multi
to 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 user2. If user creation fails, return an error3. If user creation succeeds, try creating an address4. If address creation fails, delete the user and return an error5. 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.
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!