Duplicating entry with belongs_to

I am trying to duplicate an existing entry with a different :belongs_to value but am receiving the following error:

** (ArgumentError) cannot change belongs_to association item_type because there is already a change setting its foreign key item_type_id to 1

Here is the relevant code:
Migrations:

    create table(:item_type) do
      add :name, :text
    end

    create table(:item) do
      add :name, :text
      add :item_type_id, references(:item_type, on_delete: :nothing)
    end

Schema:

  schema "item_type" do
    field :name, :string
    has_many :items, Item
  end

  schema "item" do
    field :name, :string
    belongs_to :item_type, ItemType
  end

And the get/insert code that generates the error:

  def changeset(item, params \\ %{}) do
    item
    |> cast(params, [:name, :item_type_id])
    |> validate_required([:name])
  end

  def get_insert(attrs) do
    Repo.get(Item, attrs.id)
    |> Repo.preload(:item_type)
    |> Item.changeset(%{item_type_id: attrs.item_type_id})
    |> Repo.insert()
  end

The changeset and changeset.data generated by the above get_insert function:

#Ecto.Changeset<
  action: nil,
  changes: %{item_type_id: 1},
  errors: [],
  data: #Bevdb.Data.Item<>,
  valid?: true
>
%Bevdb.Data.Item{
  __meta__: #Ecto.Schema.Metadata<:loaded, "item">,
  id: 1,
  item_type: %Bevdb.Data.ItemType{
    __meta__: #Ecto.Schema.Metadata<:loaded, "item_type">,
    id: 2,
    items: #Ecto.Association.NotLoaded<association :items is not loaded>,
    name: "Type One"
  },
  item_type_id: 2,
  name: "Item One"
}

Any help is appreciated. I am new to Elixir and struggling a bit with associations.

Let me know if I can provide any additional information.

Thanks!

You do not need to preload the :item_type, and I suspect (without checking, sorry!), that this is the problem: it has preloaded the :item_type struct, but then it tries to change the :item_type_id which means that the id’s no longer match … and when it goes to Repo.insert the record it is trying to change both the item’s type id and insert an item_type record…

Try leaving out the the Repo.preload in get_insert and that should allow it to just set the :item_type_id on its own.

Thanks for the suggestion, but removing the preload gives the error:

** (RuntimeError) attempting to cast or change association item_type from Bevdb.Data.Item that was not loaded. Please preload your associations before manipulating them through changesets

Ah, right, because you are loading the struct from the DB first, and then not updating it be trying to re-insert the whole thing. So ecto is seeing an existing item and passing that to insert as if it were new …

Instead, create a new Item altogether. Call Map.from_struct on the loaded item, set the :item_type_id on that map, and then pass the whole thing into the changeset function … this will get you a new struct you can insert, sth like:

 def get_insert(attrs) do
    changes = Repo.get(Item, attrs.id)
      |> Map.from_struct()
      |> Map.put(:item_type_id, attrs.item_type_id)

    Item.changeset(%Item{}, changes)
    |> Repo.insert()
  end

Thank you! Your solution worked well.