# Cascade Soft-Deletes
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](#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:
```elixir
# comment.ex
schema "comments" do
soft_deletes()
end
# post.ex
schema "posts" do
has_many :comments, Comment, on_delete: :delete_all
end
```
These `Post` configurations would NOT cascade:
```elixir
# Do not cascade (default association behaviour)
schema "posts" do
has_many :comments, Comment
end
```
```elixir
# Do not cascade (explicit)
schema "posts" do
has_many :comments, Comment, on_delete: :nothing
end
```
```elixir
# Do not cascade (parent row still exists, so there's nothing to nilify)
schema "posts" do
has_many :comments, Comment, on_delete: :nilify_all
end
```
## Controlling cascades
By default, cascading is disabled. A soft-delete call without `cascade: true`
only deletes the matched record:
```elixir
Repo.soft_delete(post)
```
Enable cascading for a specific call:
```elixir
Repo.soft_delete(post, cascade: true)
```
You can also skip specific associations:
```elixir
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:
```elixir
@hard_delete_on_cascade [:ratings]
schema "post" do
has_many :comments, Comment, on_delete: :delete_all
has_many :ratings, Rating, on_delete: :delete_all
end
```
```elixir
Repo.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.