首页 > 解决方案 > 使用 Ecto.Multi 更新父记录和动态子记录

问题描述

我有两个对象,日志和块。日志属于块 - 如果一个日志和一个块共享一个标签,那么它们将被链接在一起。该链接存储在日志中。

更新日志相当简单。但是,如果您在块中编辑主题标签,则必须更新链接的日志并(可能)更新将链接的日志

为了解决这个批量更新,我将通过Ecto.Multi. 但是,我不确定执行此操作的最佳方法。这是我当前的版本:

def update_block_then_logs( %Block{} = block, attrs, for: user ) do
  Ecto.Multi.new()
  |> Ecto.Multi.update( :block, Blocks.update_block(block, attrs) )
  |> Ecto.Multi.run( :logs, fn _repo, %{block: block} ->
    tags = block.tags |> String.split

    # Returns logs that are currently set to this block and logs that will be set
    log_changesets = list_logs_affected_by( block: block, tags: tags, for: user )
    |> Enum.map( fn log ->
      regenerate_blocks_for_log( log, for: user )
    end)

    errors = log_changesets
    |> Enum.filter( fn {status, _changeset} -> status == :error end )
    |> Enum.map( fn {_status, changeset} -> changeset end )

    if errors != nil && Enum.count(errors) > 0 do
      { :error, errors }
    else
      { :ok, log_changesets }
    end
  end)
  |> Repo.transaction
end

regenerate_blocks_for_log询问数据库(并假设 Block 已经更新)。它最终调用Repo.update( log_changeset )而不是返回变更集本身。因为我正在使用Ecto.Multi.run我必须创建一些错误捕获。我写的代码看起来不错,但后来我认为如果我将每个代码分离Repo.update到对象中自己的操作中,可能会更好地捕获错误Ecto.Multi

所以我创建了这个版本:

def update_block_then_logs( %Block{} = block, attrs, for: user ) do
  block_changeset = Blocks.update_block( block, attrs )

  multi = Ecto.Multi.new()
  |> Ecto.Multi.update( :block, block_changeset )

  tags = block_changeset 
  |> Ecto.Changeset.get_field( :tags )
  |> String.split

  # Returns logs that are currently set to this block and logs that will be set
  multi_log = list_logs_affected_by( block: block, tags: tags, for: user )
  |> Enum.map( fn log ->
    op_name = String.to_atom("log_#{log.id}")
    log_changeset = regenerate_blocks_for_log( log, for: user )

    Ecto.Multi.new
    |> Ecto.Multi.update( op_name, log_changeset )
  end)
  |> Enum.reduce( Ecto.Multi.new(), &Ecto.Multi.append/2 )

  Ecto.Multi.append( multi, multi_log )
  |> Repo.transaction()
end

在这个版本中,regenerate_blocks_for_log返回一个变更集。但是,我意识到这regenerate_blocks_for_log是行不通的——因为它是为了读取事务中提交的数据而编写的(直到最后一条语句才会发生)。

我可以进行调整regenerate_blocks_for_log,以便将尚未提交的 Block 更改考虑在内。但后来我想知道,有没有办法Multi使用第一种技术动态改变(因为记录已在事务中提交)?

标签: phoenix-frameworkecto

解决方案


基于此问题的原始帖子中的答案。Ecto.Multi.merge允许您附加到Multi事务中的 while:

defp update_logs( block, for: user ) do
  tags = if block.tags == nil do [] else block.tags |> String.split end

  # Returns logs that are currently set to this block and logs that will be set
  list_logs_affected_by( block, tags, for: user )
  |> Enum.map( fn log ->
    op_name = String.to_atom("log_#{log.id}")
    log_changeset = regenerate_blocks_for_log( log, for: user )

    Ecto.Multi.new
    |> Ecto.Multi.update( op_name, log_changeset )
  end)
  |> Enum.reduce( Ecto.Multi.new(), &Ecto.Multi.append/2 )
end

def update_block_then_logs( %Block{} = block, attrs, for: user ) do
  Ecto.Multi.new()
  |> Ecto.Multi.update( :block, Blocks.update_block(block, attrs) )
  |> Ecto.Multi.merge( fn %{block: block} -> update_logs(block, for: user) end )
  |> Repo.transaction
end

推荐阅读