使用Elixir递归的真实世界用例
最近,我正在为我们的一个客户使用Elixir编写一个后台工作者。有一个需求是更新一个数据库表中的记录,同时更新另一个表中的许多记录。这篇文章是关于我如何使用递归来解决这个用例的。
我正在开发一个应用程序,每个账户都有订单。后台工作人员将搜索所有待处理的订单,并将其标记为已完成,然后更新这些相关的账户。
我在Elixir中搜索类似于Rails中find_in_batches的东西,发现了这个讨论。
我发现Ecto.Repo.stream/2可以遍历每个订单,更新其状态,然后更新与这些订单相关的账户。它可以按以下方式完成:
defmodule BalanceUpdateWorker do
alias Bank.Order
alias Bank.Account
alias Bank.Repo
import Ecto.Query
@pending 0
@completed 1
def perform do
Repo.transaction(fn ->
Repo.stream(orders_query())
|> Enum.each(fn order ->
{:ok, updated_order} = update_order(order)
{:ok, _} = update_account(updated_order)
end)
end)
end
defp orders_query() do
from(order in Order,
where: order.status == ^@pending
)
end
defp update_order(order) do
order
|> Order.changeset(%{status: @completed})
|> Repo.update()
end
defp update_account(order) do
account = Repo.get_by(Account, user_id: order.user_id)
account
|> Account.changeset(%{amount: Decimal.add(account.amount, order.amount)})
|> Repo.update()
end
end
然而,上述方法有一个问题。当更新记录的时间超过超时时,Ecto将引发超时错误,如下所示:

另外,所有的记录更新都会回滚。
在Repo.stream/2 ,SQL适配器只能在一个事务内枚举一个流。
我意识到,与其让所有记录包裹在一个数据库事务内,不如为每个记录的更新设置一个数据库事务,这样可以解决数据库超时的问题。
在没有Repo.stream/2 的新方法中,我将不得不自己处理find_in_batches,即通过迭代一批记录,然后继续处理下一批。这是一个递归的使用案例。
使用递归,我可以有一个方法,只要待处理的记录数大于批次大小,就可以多次调用它。
首先,我们需要知道有多少条记录有待处理,然后将这个数字传递给一个方法,该方法将处理这批记录,然后再次调用自己:
@batch_size 500
def perform do
remaining_records_count()
|> iterate_multiple_times()
end
defp remaining_records_count do
orders_query()
|> Repo.aggregate(:count)
end
defp iterate_multiple_times(count) when count <= @batch_size,
do: make_account_balance_available()
defp iterate_multiple_times(_count) do
make_account_balance_available()
remaining_records_count()
|> iterate_multiple_times()
end
defp orders_query_with_limit do
from(order in Order,
where: order.status == ^@pending,
limit: ^@batch_size
)
end
defp make_account_balance_available do
orders_query_with_limit()
|> Repo.all()
|> Enum.each(fn order ->
{:ok, updated_order} = update_order(order)
{:ok, _} = update_account(updated_order)
end)
end
在上面的重构中,perform/0 ,找出要更新的记录数,并将其传递给iterate_multiple_times/1 递归函数。如果计数超过500,它将处理该批记录,然后计算剩余的记录并调用自己,如此循环。
更新后的代码成功运行:
