如何正确编写交易并冒出错误

How to correctly write a transaction and bubble up the errors

好的,因为 return 的多个级别,我有点迷路了。

我对 ecto 很陌生,所以开始吧。

我正在尝试将我的帐户创建包装在交易中,因为它会创建许多子记录等。

到目前为止我有这个:

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

ecto 模式模型上的 create_account 看起来像:

Account.ex

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

所以现在有 3 个级别的 return 值,我不确定如何一起处理这些值:

  1. 交易的快乐路径似乎是return: {:好的,模型}

  2. 如果 account.create_account 插入失败,如何将错误传递给最终的 return 值,以便我可以在 UI 中显示它?

  3. 如何在任何步骤中正确回滚?

使用 Kernel.SpecialForms.with/1 类似 monad 的特殊形式:

def create_account(company_name, ...) do
  Repo.transaction(fn ->      
    with {:ok, account} <- Account.create_account(...),
         {:ok, _} <- AnotherModel.create_record(...),
         ...
         {:ok, _} <- LastModel.create_record(...) do
      IO.puts("All fine")
      account
    else
      error ->
        IO.inspect(error, label: "Error happened") 
        Repo.rollback(:error_in_transaction)
    end
  end) # transaction
end

您应该在失败时使用 Repo.rollback。文档说 The transaction will return the value given as {:error, value},所以这可以通过你提到的模式匹配来完成:

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

这样,您的函数将 return {:ok, account} 成功,并 {:error, changeset} 遇到任何失败。因为您要插入多个东西,所以您可能想要区分它们,可能像这样:

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

在这种情况下,如果一切正常,函数将 return {:ok, account},如果帐户插入失败,{:error, {:account, account_changeset}},如果用户插入失败,{:error, {:user, user_changeset}}

您的意图描述听起来像是 Ecto.Multi 的完美用例。它是 Ecto 的一个特性,允许您定义复杂的数据处理管道。有详细的解释和更多的例子here,但总体思路简单而扎实。

account = Account.changeset(%Account{}, params)
subscription = %Subscription{valid_until: ~D[2020-09-30]}

create_account =
  Ecto.Multi.new()
  |> Ecto.Multi.insert(:insert_account, account)
  |> Ecto.Multi.run(:insert_subscription, fn repo, %{insert_account: account} ->
       subscription
       |> Map.put(:account_id, account.id)
       |> repo.insert()
     end)

Repo.transaction(create_account)

随时根据自己的喜好重构它;基本思想是每个步骤都定义为 shorthand 操作,例如 insert 或定义为 returns {:ok, record}{:error, _} 的函数 - 如 Multi.run 因为它需要引用上一步的工件。

管道在create_account变量中定义,然后它只在调用Repo.transaction(create_account)时执行。这样,所有步骤都 运行 作为单个事务。

  • 如果所有步骤都成功,返回{:ok, %{insert_user: %User{...}, insert_subscription: %Subscription{...}},事务提交。
  • 如果任何步骤失败(对于定义为函数的步骤,则意味着返回 {:error, _}),将返回一个错误元组,例如 {:error, :insert_user, %Ecto.Changeset{}} - 事务将回滚。在这种情况下,失败发生在 insert_user 步骤。