Search code examples
elixirphoenix-frameworkecto

How to correctly write a transaction and bubble up the errors


Ok because of the multiple levels of returns I am getting a little lost.

I am very new to ecto so here goes.

I'm trying to wrap my account creation in a transaction because it creates many child records etc.

So I have this so far:

def create_account(company_name, ...) do
  Repo.transaction(fn ->      
    case Account.create_account(%{
          # ... attributes here
        }) do
          ????
        end

        # insert other model records here using the same above case pattern matching

    account
  end) # transaction
end

The create_account on the ecto schema model looks like:

Account.ex

def create_account(attrs \\ %{}) do
  %Account{}
  |> Account.changeset(attrs)
  |> Repo.insert()
end

So now there are 3 levels of return values which I am not sure how to handle all together:

  1. the happy path of a transaction seems to return: {:ok, model}

  2. if the account.create_account insert fails, how to pass that errors down to the final return value so I can display that in the UI?

  3. how to correctly rollback in any of the steps?


Solution

  • You should use Repo.rollback on failures. The docs say The transaction will return the value given as {:error, value}, so this can be done with pattern matching as you mention:

    def create_account(company_name, ...) do
      Repo.transaction(fn ->
        account = case Account.create_account(%{ # ... attributes here }) do
          {:ok, account} -> account
          {:error, changeset} -> Repo.rollback(changeset)
        end
    
        # insert other model
    
        {:ok, account}
      end)
    end
    

    This way your function will return {:ok, account} on success, and {:error, changeset} on whatever failure it encounters. Because you're inserting multiple things you might want to differentiate them, maybe like so:

    account = case Account.create_account(%{ # ... attributes here }) do
      {:ok, account} -> account
      {:error, changeset} -> Repo.rollback({:account, changeset})
    end
    
    case User.create_user(account, %{ # ... attributes here }) do
      {:ok, user} -> :ok
      {:error, changeset} -> Repo.rollback({:user, changeset})
    end
    

    In this case the function will return {:ok, account} if everything goes right, {:error, {:account, account_changeset}}, if account insertion fails, and {:error, {:user, user_changeset}} if user insertion fails.