Ecto's :on_replace :delete option doesn't work for has_many association

I repost my question on stackoverflow here in hope to get it answered.
I’ve got schema with has_many association with :on_replace option set to :delete. And I’ve got the form with a group of checkboxes to submit new values to be put as associations to the parent entity. I want all previously set associations to be removed on edit, but instead I get new associations inserted into the database and old still persist in the database.

The changeset after submit has only new associations without id’s in it’s changes, like this:

%{materials: [#Ecto.Changeset<action: :insert,
    changes: %{material: "Cast iron"}, errors: [],
    data: #HrPro.Entities.Material<>, valid?: true>,
    #Ecto.Changeset<action: :insert,
    changes: %{material: "Steel"}, errors: [],
    data: #HrPro.Entities.Material<>, valid?: true>]}

As you can see there are no :id fields in the changeset, so I expected that previously saved materials will be deleted from database but this is not the case.

The schema looks like this:

schema "entities" do
  has_many :materials, Material, on_replace: :delete
end

def changeset(%Entity{} = entity, attrs \\ %{}) do
  entity
  |> cast(attrs, @allowed_fields)
  |> cast_assoc(:materials)
end 

My installation is:

phoenix 1.3
ecto 2.2.6

Can we make Ecto to clean all previously submitted associated values from the database on edit?

Newbee here, so not sure if I can help.

What’s the code of the changeset? Is cast_assoc present in that changeset code?

Hi, yes, cast_assoc is present, of course. New values get saved to the database, the problem is that old ones are not deleted.
I added code of the schema to the topic.

I think you have to preload the current materials of each entity. Something like:

entity = Repo.one(from e in Entity, where: e.id==1, preload: :materials)

For example say that you have one material in that entity:

iex> entity
%HrPro.Entities.Entity{
  __meta__: #Ecto.Schema.Metadata<:loaded, "entities">,
  id: 1, name: "spaceship", 
  materials: [
    %HrPro.Entities.Material{
       __meta__: #Ecto.Schema.Metadata<:loaded, "materials">,
       id: 1, material: "iron", entity_id: 1]
}

Then when you make any changes in the changeset, say you add the material carbon, expecting that the iron will be removed, then the changeset should be something like:

%{materials: [
    #Ecto.Changeset<action: :replace, changes: %{material: "Iron"}, errors: [], data: #HrPro.Entities.Material<>, valid?: true>,
    #Ecto.Changeset<action: :insert, changes: %{material: "Carbon"}, errors: [], data: #HrPro.Entities.Material<>, valid?: true>
]}

See the iron material being “replaced”? I think that’s the key and that if the material with the :replace action is missing, not having the iron material deleted from DB is the expected behaviour.

Hope it helps!

Thanks for your efforts! But I checked and all materials were preloaded in my changeset. All they are present in the changeset.data. I thought that was the idea behind on_replace: :delete, to check all id’s of materials present in the changeset.data and absent in changeset.changes and delete all such entries from the database, but it doesn’t happen.

Thanks again, Ivan, your answer actually helped me to spot the problem. I’ve missed that in your changes replaced material is present and in mine it’s not. So there is something wrong with my changeset’s logic, I used new action: :ignore feature of Ecto.Changeset to not allow ecto pollute my database with empty materials when corresponding checkbox is not checked and it simply threw this change out of list of changes… Will need to rework all this.