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

时间:2018-12-19 17:24:36

标签: elixir phoenix-framework ecto

好吧,由于回报率多种多样,我有点迷失了。

我对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个级别的返回值,我不确定如何一起处理所有这些值:

  1. 交易的快乐路径似乎返回了: {:ok,model}

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

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

3 个答案:

答案 0 :(得分:2)

失败时,您应该使用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

这样,您的函数将在成功时返回{: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

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

答案 1 :(得分:2)

您的意图描述听起来像是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)

根据您的喜好随意重构它;基本思想是将每个步骤定义为诸如insert之类的速记操作,或者定义为返回{: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步骤中。

答案 2 :(得分:1)

使用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