如何正确编写交易并冒出错误
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 值,我不确定如何一起处理这些值:
交易的快乐路径似乎是return:
{:好的,模型}
如果 account.create_account 插入失败,如何将错误传递给最终的 return 值,以便我可以在 UI 中显示它?
如何在任何步骤中正确回滚?
使用 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
步骤。
好的,因为 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 值,我不确定如何一起处理这些值:
交易的快乐路径似乎是return: {:好的,模型}
如果 account.create_account 插入失败,如何将错误传递给最终的 return 值,以便我可以在 UI 中显示它?
如何在任何步骤中正确回滚?
使用 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
步骤。