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.
→ 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.

    |> 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)


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

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])

  user_id: base.user_id,
  # other columns from base
  fields:, &copy_field/1),
  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
    # ... copy columns as needed

# 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.


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.


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

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

Hope that helps.