How to make forms and transactions play well in ph

2020-07-27 02:15发布

问题:

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

回答1:

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?



回答2:

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)