A Quick Guide to the Complex: Ecto.Multi

Ecto.Multi, a data structure added in Ecto 2.0, is an extremely useful tool for creating and executing complex, atomic transactions. This very brief guide will cover a few of the most useful methods associated with Ecto.Multi and when to use them.

Common Uses

insert(multi, name, changeset_or_struct, opts \\ [])
The most straightforward way to use Ecto.Multi is to chain individual changesets together. insert, update, and delete functions are available and all behave as you might expect them to, with all operations are executed in the order in which they are added. You can imagine a transaction dealing with a user signing up via an invitation email might look something like this:

Ecto.Multi.new
|> Ecto.Multi.insert(:user, user_changeset)
|> Ecto.Multi.delete(:invitation, invitation)
|> Repo.transaction()

What might have been be two separate database transactions has been condensed into a single, atomic transaction, with Ecto.Multi handling rollbacks when necessary. But what about when one operation relies on the results of a previous one?


Run

run(multi, name, fun)

Run is an extremely versatile method that adds a function to the Ecto.Multi transaction.  

The function must return a tuple in the for of {:ok, value} or {:error, value}, and, 

importantly, is passed a map of the operations processed so far in the Multi struct. This means we can key into the changes created by previous operations, and use the those values while executing any code we like. The transaction creating and sending the invitation mentioned above could look something like this:

Ecto.Multi.new()

|> Ecto.Multi.insert(:invitation, invitation_changeset)

  |> Ecto.Multi.run(:send_invite_email, fn multi_map ->

  send _invite_email(multi_map)

end)

    |> Repo.transaction()

Append

append(lhs, rhs)
append is a handy way to combine two Ecto.Multi structs into a single atomic transaction. One potential pattern is to compose multiple functions that return Ecto.Multi structs, and combine them as needed. As noted above, operations are executed in order, so if you want the appended struct to be executed first, you’ll want to use prepend instead.

def create_and_send_invite(invitation_changeset)

Ecto.Multi.new()

|> Ecto.Multi.insert(:invitation, invitation_changeset)

  |> Ecto.Multi.run(:send_invite_email, fn multi_map ->

  send _invite_email(multi_map)

end)

end


def clear_expired_invites()

    Ecto.Multi.new()

        |> Ecto.Multi.run(:clear_invites, fn () -> 

          clear_invites()

        end)

end


def invite_user(inviation_changeset)

invite_multi = create_and_send_invite(invitation_changeset)

clear_expired = clear_expired_invites()


Ecto.Multi.new()

|> Ecto.Multi.append(invite_multi)

|> Ecto.Multi.append(clear_expired)

|> Repo.transaction()

end

Merge

merge(multi, fun)

Similar to run, merge will execute a given function and any arbitrary code associated with that function. Unlike run, this function is expected to return a multi struct whose operations will be merged into the multi struct passed to it as the first parameter.

def add_user_to_organization(multi_map, organization)

    user = multi_map[:user]


    Ecto.Multi.new()

        |> Ecto.Multi.run(:organization, organization.add_user(organization, user)

end


def create_collaborator(user_changeset, organization)

Ecto.Multi.new()

|> Ecto.Multi.insert(:user, user_changeset)

|> Ecto.Multi.merge(:add_to_org, fn (multi_map, organization) -> 

          add_user_to_organization(multi_map, organization)

        end)

|> Repo.transaction()

end


Peter Ludlum

Peter is a Software Engineering Intern at Tinfoil Security. A recent graduate of App Academy, he enjoys nothing more than bringing beautiful (and functional) web pages to life. When he isn't coding, Peter is usually lost in a book or strumming out a new tune on the ukulele.