Removing entries from `embeds_many`

There seems to be something entrancingly wrong with removing entries from an embeds_many association. Frankly there seems to be no safe way to do it.

So embedded structs contain a special id in order for one to be able to update them.
This works perfectly fine and is useful to avoid mistakes e.t.c when updating.

However when one wants to remove an entry altogether one would expect to do something like this

## Person has embeds many jobs
person = People.get(1)
person.jobs # => [%Job{id: "xxx"}, %Job{id: "xyz"}, %Job{id: "zzz"}]

change = Person.changeset(person, %{jobs: [%Job{id: "xxx"}, %Job{id: "xyz"}]})
## => ERROR!!
** (RuntimeError) you are attempting to change relation :jobs of
Person, but there is missing data.

If you are attempting to update an existing entry, please make sure
you include the entry primary key (ID) alongside the data.

If you have a relationship with many children, at least the same N
children must be given on update. By default it is not possible to
orphan embed nor associated records, attempting to do so results in
this error message.

It is possible to change this behaviour by setting `:on_replace` when
defining the relation. See `Ecto.Changeset`'s section on related data
for more info.

How is one expected to delete an entry with the ID and N restrictions?
One way I have figured is to transform the map to a bjson string and then run the update with Repo.update_all but that so dirty I don’t even want to talk about it.

What is the correct way, is this a shortcoming of the current implementation?

I think you need to set on_replace: :delete on your embed_many relation.

1 Like

I see, so use on_replace: :delete and take the responsibility as a developer that you don’t remove desired entries. Isn’t that a little bit error prone? Would a Ecto.Changeset.remove_embed function make sense for the future? I think it could be a better solution, and would certainly keep a lot of data safer from developers.

I see, so use on_replace: :delete and take the responsibility as a developer that you don’t remove desired entries. Isn’t that a little bit error prone?

Indeed, using on_replace: :delete is dangerous but for the API you wanted above that’s the solution. I should have mentioned that a safer solution, which is actually recommended and documented by Ecto, is to use a virtual delete field on embedded schema’s changeset function. Perhaps that would be more appropriate for your use case.

See: https://hexdocs.pm/ecto/Ecto.Changeset.html#module-on-replace (mentioned by https://hexdocs.pm/ecto/Ecto.Schema.html#embeds_many/3)

1 Like

I believe the :on_replace rule only applies to cast_assoc, which is about mapping external parameters to your in memory representation. If you have more direct control over the data, you can use Ecto.Changeset.put_assoc and that won’t trigger safety mechanisms as on_replace.

1 Like

put_assoc seems not to be appropriate for embeds

person
|> Ecto.Changeset.change() 
|> Ecto.Changeset.put_assoc(:jobs, [])
## -> ** (ArgumentError) expected `jobs` to be an assoc in `put_assoc`, got: `embed`

@wojtekmach the soft delete approach sounds great actually :slight_smile: , and you can always use on_replace: :delete if you really want to get rid of something, alright thanks for your time, I know it’s worth much to you

Sorry, I meant to say put_embed.

1 Like

owww yes :smiley: that works

also while writing the post I totally forgot that Person.changeset is a function that the developer creates