Help me understand the associations `on_replace: :delete` example in the docs

In Ecto.Changeset — Ecto v3.11.1 there is a part which says:

The :delete option in particular must be used carefully as it would allow users to delete any associated data. If you need deletion, it is often preferred to add a separate boolean virtual field to the changeset function that will allow you to manually mark it for deletion, as in the example below: […]

defmodule Comment do
  use Ecto.Schema
  import Ecto.Changeset

  schema "comments" do
    field :body, :string
    field :delete, :boolean, virtual: true
  end

  def changeset(comment, params) do
    cast(comment, params, [:body, :delete])
    |> maybe_mark_for_deletion
  end

  defp maybe_mark_for_deletion(changeset) do
    if get_change(changeset, :delete) do
      %{changeset | action: :delete}
    else
      changeset
    end
  end
end

However, this example is incomplete and I am having trouble applying that.

Carrying on with the comment theme, If I have this as the schema that inserts it with cast_assoc, where can I mark it as deleted? :

(I know it’s weird to have one comment but that’s the type of relationship I’m using in my actual code, so I want to keep the example similar)

defmodule Post do
  use Ecto.Schema
  import Ecto.Changeset

  schema "posts" do
    field(:title, :string)
    field(:body, :string)
    has_one(:comment, Comment)
  end

  def update_changeset(post, attrs) do
    post
    |> Repo.preload(:comment)
    |> cast(attrs, [:title, :body])
    |> cast_assoc(:comment, required: true, on_replace: :nilify)
  end
end

The complete explanation is here

so in schema “comments” you have correctly:
field :delete, :boolean, virtual: true
and if you update a Post containing a Comment and this comment has the value ‘true’ for the ‘delete’ field, it will be marked for deletion, and it will be deleted.

I did find the name ‘on_replace’ confusing. It specifies what to do when you don’t send an update for a child that does exists in the database. I would call it something like: ‘on_missing’ or ‘on_no_update_for’.
And the only values it can have are :raise (default), :mark_as_invalid, :nilify, :update, or :delete.
Maybe it’s me, but what I want is an option like :ignore / :nothing.

But won’t that mark the latest comment for deletion?

I want the one being replaced to be marked for deletion…

In your case a post can have only one comment, so there is only one comment that could be deleted.
So either you update the Post with the Comment with delete=true in the comment, and that comment (with the same id) will be delete,
or you specify on_replace: :delete and update the Post without any comments or with a new comment, and the on_replace will delete the existing comment.

1 Like