使用Ecto.Multi更新父记录和动态子记录

时间:2019-01-22 11:12:53

标签: phoenix-framework ecto

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

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

为了解决此批量更新,我将通过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询问数据库(并假定该块已被更新)。它最终会调用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(因为记录已在事务中提交)?

1 个答案:

答案 0 :(得分:0)

基于original post中对此问题的回答。 Ecto.Multi.merge允许您在事务内追加到Multi

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