When you soft-delete record(s), you can cascade soft-delete relationships.
Lazarus cascades only when the delete runs with cascade: true. Cascading is
disabled by default, so Lazarus otherwise soft-deletes only the records matched
by the delete call.
When cascading is enabled, Lazarus follows schema association metadata recursively in a transaction:
| Association option | Soft-delete mode |
|---|---|
on_delete: :delete_all | soft-deletes the child branch |
on_delete: :nilify_all | noops because the parent row still exists |
on_delete: :nothing | noops |
If a related schema is being soft-deleted via on_delete: :delete_all but does
not include deleted_at, the operation raises ArgumentError. It will NOT
silently fail, skip, or hard delete.
Related schemas do not need deleted_at when that branch is hard-deleted via
@hard_delete_on_cascade (see
Forcing hard delete during cascades).
If any of the soft-delete cascade branches fail, the whole transaction will be rolled back.
Schema examples
Suppose we have a Post which has many Comment's
If we want Repo.soft_delete(post, cascade: true) to cascade soft-delete all
comments that belong to that post, we'd need to set them up like so:
# comment.ex
schema "comments" do
soft_deletes()
end
# post.ex
schema "posts" do
has_many :comments, Comment, on_delete: :delete_all
endThese Post configurations would NOT cascade:
# Do not cascade (default association behaviour)
schema "posts" do
has_many :comments, Comment
end# Do not cascade (explicit)
schema "posts" do
has_many :comments, Comment, on_delete: :nothing
end# Do not cascade (parent row still exists, so there's nothing to nilify)
schema "posts" do
has_many :comments, Comment, on_delete: :nilify_all
endControlling cascades
By default, cascading is disabled. A soft-delete call without cascade: true
only deletes the matched record:
Repo.soft_delete(post)Enable cascading for a specific call:
Repo.soft_delete(post, cascade: true)You can also skip specific associations:
Repo.soft_delete(post, cascade: true, skip_associations: [:comments])Forcing hard delete during cascades
Use @hard_delete_on_cascade when a particular association should always be
physically deleted even though the parent delete is a soft delete:
@hard_delete_on_cascade [:ratings]
schema "post" do
has_many :comments, Comment, on_delete: :delete_all
has_many :ratings, Rating, on_delete: :delete_all
endRepo.soft_delete(post, cascade: true)In the example above, :comments are soft-deleted if they support soft
deletion, while :ratings are hard-deleted.
Associations listed in @hard_delete_on_cascade do not need deleted_at.
@hard_delete_on_cascade only switches associations that also use
on_delete: :delete_all; it does not override on_delete: :nilify_all.
Once Lazarus enters a hard-delete branch, descendant associations follow their own schema metadata recursively:
| Association option | Hard-delete mode |
|---|---|
on_delete: :delete_all | hard-deletes the child branch |
on_delete: :nilify_all | nulls the child foreign key |
on_delete: :nothing | noops and database constraints may reject the hard delete |
Additional notes
Keep cascade rules in schemas, not only in database migrations: soft-delete behavior is driven by Ecto schema metadata, not by database FK actions, so it's recommended that you keep cascade rules in schemas rather than relying on migrations/database only. Keep in mind defining them in both places can easily drift out of sync.