I'm playing with Phoenix + Ecto and I stumbled upon something that does not feel idiomatic to me.
I have a form that represents an Invitation
. When creating an Invitation we also need to create a User
and obviously I want both to happen in a transaction so I keep data consistency. In my form I ask for name
and email
.
Since I want the Invitation
changeset in my view to represent the errors correctly I ended up with this code... but does not look great.
Do you know a better way to do this in Phoenix + Ecto?
def create(params) do
Repo.transaction(fn ->
case Repo.insert(User.email_changeset(%User{}, params)) do
{:ok, user} ->
changeset = Invitation.changeset(%Invitation{}, params)
case Repo.insert(Ecto.Changeset.change(changeset, user_id: user.id)) do
{:ok, user} ->
user
{:error, changeset} ->
Repo.rollback(changeset)
end
{:error, _changeset} ->
Repo.rollback(%{Ecto.Changeset.add_error(changeset, :email, "Wrong email") | action: :insert})
end
end)
end
You are looking for the with
operator. The beauty of this syntax is that if, at any point, you don't get what you're expecting, it stops the chain of commands and fires your else
block:
Repo.transaction(fn ->
with {:ok, first_object} <- create_some_object,
{:ok, second_object} <- create_another(first_object.something) do
second_object
else
{:error, error_key} ->
Repo.rollback(error_key)
end
end)
if create_some_object
doesn't return a struct matching {:ok, first_object} then the second_object is never created. Cool, right?
You can try with Ecto.Multi
.Here's an example:
defmodule Service do
alias Ecto.Multi
import Ecto
def insert_changeset(params) do
Multi.new
|> Multi.insert(:user, User.email_changeset(%User{}, params))
|> Multi.insert(:invitation, Invitation.changeset(%Invitation{}, params))
end
end
And your create function:
def create(params) do
Service.insert_changeset(params)
|> Repo.transaction
end
Or you can pattern matching to make your code nicer
def create(params) do
Repo.transaction(fn ->
changeset = User.email_changeset(%User{}, params)
changeset
|> Repo.insert
|> invitation_insert(params)
end)
end
defp invitation_insert({:error, changeset}, _params), do: Repo.rollback(changeset)
defp invitation_insert({:ok, _}, params) do
Invitation.changeset(%Invitation{}, params)
|> Repo.insert
|> do_invitation_insert
end
defp do_invitation_insert({:ok, user}), do: user
defp do_invitation_insert({:error, changeset}), do: Repo.rollback(changeset)