How to make forms and transactions play well in ph

2020-07-27 02:35发布

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

2条回答
做自己的国王
2楼-- · 2020-07-27 02:55

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?

查看更多
对你真心纯属浪费
3楼-- · 2020-07-27 03:04

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)
查看更多
登录 后发表回答