I have two objects, Logs and Blocks. Logs belong to Blocks - if a Log and a Block share a hashtag, then they’ll be linked together. The link is stored in the Log.
Updating Logs is fairly simple. However, if you edit the hashtags in a Block, then you have to update linked Logs and (potentially) update Logs that will be linked
To address this bulk update, I’m wrapping up database operations in a transaction via Ecto.Multi
. However, I’m not sure of the best way to do this. This is my current version:
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
interrogates the database (and will assume that the Block has already been updated). It ends up calling Repo.update( log_changeset )
rather than returning the changeset itself. Because I’m using Ecto.Multi.run
I had to create some error trapping. The code I wrote seems okay, but then I thought it might be better for error trapping if I separated out each Repo.update
into its own operation in the Ecto.Multi
object.
So I created this version:
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
In this version, regenerate_blocks_for_log
returns a changeset. However, I realised that regenerate_blocks_for_log
won’t work - since it was written to read data committed within a transaction (which doesn’t happen until the last statement).
I could adjust regenerate_blocks_for_log
so that it takes into account changes to the Block that have not yet been committed. But then I wonder, is there a way to dynamically alter the Multi
using the first technique (as records have been committed in the transaction)?