使用Elixir递归的真实世界用例

104 阅读2分钟

使用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将引发超时错误,如下所示:

screenshot1

另外,所有的记录更新都会回滚。

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,它将处理该批记录,然后计算剩余的记录并调用自己,如此循环。

更新后的代码成功运行:

screenshot2