Copy record with all associations

How can I copy a record to a new record with all associated records.
I have a Template, that has Fields and Keywords associations.
Template
→ Fields
→ Keywords

I want to create a duplicate of Template, and also duplicates of all associated records.
Tried with preloading everything and deleting :id so it signals as a new record.

Template
    |> Repo.get!(id)
    |> Repo.preload(:user)
    |> Repo.preload(:fields)
    |> Repo.preload(:keywords)
    |> Map.delete(:id)
    |> Map.delete(:template_id)
    |> Repo.insert!

I get ** (KeyError) key :id not found error.

Try setting the :id to nil instead of deleting it.

1 Like

It creates a new Template, but all associations are only moved to a new Template. I want to copy also all associations, beside user, which is on top of Template.

I also tried with conversion to a struct.

changes = Repo.get(Template, id)
      |> Repo.preload(:fields)
      |> Map.from_struct()

and then modifying struct with

changes = changes
      |> Map.put(:id, new_template_uuid)

and

changes = update_in(changes, [:fields, Access.all(), :template_id], fn note ->
   %{note | template_id: new_template_uuid}
end)

but get an error:

... Templates.TemplateField does not implement the Access behaviour

It may be more legible to explicitly copy what you want copied instead of trying to mutate pre-existing Ecto structs into an insertable shape.

base = Repo.get(Template, id) |> Repo.preload([:fields, :keywords])

%Template{
  user_id: base.user_id,
  # other columns from base
  fields: Enum.map(base.fields, &copy_field/1),
  keywords: Enum.map(base.keywords, &copy_keyword/1)
}
|> Repo.insert!

# helper functions; could also live on TemplateField etc if the operation is complicated

defp copy_field(base_field) do
  %TemplateField{
    # ... copy columns as needed
  }
end

# similar for copy_keyword/1

This will require more effort than the map-manipulation approach when adding new columns, but on the other hand it will never copy values that weren’t expected.

2 Likes

Yes, it was way faster to solve this way. Thanks!
But it would be interesting to get a solution for generic master-associated record copy/ing method.

Regards,
Gregor

In other not to cause conflict when trying to clone the record , it’s best you make a soft copy by copying only the attributes you need and casting the attributes into a changeset, then inserting.

duplicate_record_from_target_recrod = fun target_record -> 
  #note don't include the id!
  target_fields = [:fields,:keywords,:associated_record_id] 
  Template.changeset(Map.take(target_record, target_fields))
  |> Repo.insert
end

USE CASE
template = Template |> Repo.get(id)
duplicate_record_from_target_recrod.(template)
#create a new template with all associated records.

Hope that helps.