How do I use Ecto.Multi to create conditional transactions for a has_many / belongs_to relationship?

I have two models and associated database tables: User and Address. A user has_many addresses and an address belongs_to a user (so one of its required fields is user_id). I am working on a user registration endpoint which requires a user to send over user details and address details in order to complete the registration process. The registration params comes over like this:

{
  “user”: {
    “email”: “admin@example.com”,
    “password”: “password1”,
    “phone_number”: “2122122121”
  },
  “shipping_address”: {
    “city”: “Cambridge”,
    “country”: “US”,
    “postal_code”: “02139”,
    “state_province”: “MA”,
    “street_line1”: “111 Example St.”,
    “street_line2”: “Suite 500”
  }
}

I’m nesting a couple case statements and reject the registration if either the user or address fails to create:

def create(conn, %{"user" => user_params, "address" => address_params}) do
  user_changeset = User.changeset(%User{}, user_params)

  case Repo.insert(user_changeset) do
    {:ok, user} ->

      address_params = Map.merge(address_params, %{"user_id" => user.id})
      address_changeset = Address.changeset(%Address{}, 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, address_changeset} -> 
          Repo.delete(user)

          conn
          |> put_status(:unprocessable_entity)
          |> render(MyApp.RegistrationView, "error.json", changeset: address_changeset)
      end

    {:error, changeset} ->
      conn
      |> put_status(:unprocessable_entity)
      |> render(MyApp.RegistrationView, "error.json", changeset: changeset)
  end
end

This approach seems very flimsy to me (multiple database transactions, deleting the user if the shipping address fails, etc.). I would love to be able to wrap the whole registration process into a single transaction which can succeed or be rejected (and provide validation errors on failures). I came across Ecto.Multi, which seems like it might be a good fit here, but I’m not sure how to implement it for this use case. Any suggestions would be appreciated.

2 Likes

You ‘should’ be able to just insert both via Ecto.Multi, assuming both succeed, and if either fail then Ecto.Multi rolls back it all. Ecto.Multi is awesome and you should definitely use it (even when you don’t need transactions). :slight_smile:

1 Like

I use this pattern a lot:

Multi.new
|> Multi.insert(:user, user_changeset)
|> Multi.run(:address, fn %{user: user} ->
  address = <do something with params and user.id>
  Repo.insert(address)
end)

Then you can wrap it in Repo.transaction and check for ok/error.

10 Likes

Thanks @dom! Your suggestion was really helpful. I really like this pattern :smile_cat:

You can also use Ecto.Changeset.cast_assoc/3 in your User.changeset/2.