skip to content

In Django, how does on_delete=RESTRICT differ from on_delete=PROTECT, and when does RESTRICT still let a delete go through?

level: seniorimportance: should knowfreq 33%

answer

  1. both refuse, but not equally
  2. added in Django 3.1
  3. a second path that also cascades
  4. RestrictedError vs ProtectedError

basics

~10 s

PROTECT refuses any delete that reaches a protected row. RESTRICT refuses it too, except when the restricted rows are themselves being deleted through a CASCADE path in the same operation; then the delete proceeds.

solid answer

~40 s

Both are Python-level rules checked by Django's deletion collector before any SQL runs, and both raise a subclass of `IntegrityError`. `PROTECT` raises `ProtectedError` as soon as a delete, direct or cascaded, reaches a row that references the target. `RESTRICT`, added in Django 3.1, records the referencing rows and raises `RestrictedError` only at the end if they are not also being deleted through a `CASCADE` elsewhere in the same operation. So with a `Contract` that cascades from its publisher and restricts its book, deleting a single book is refused, but deleting the publisher removes the publisher, its books and their contracts. With `PROTECT`, that publisher delete would fail.

code

python · 19 lines
python
from django.db import models


class Publisher(models.Model):
    name = models.CharField(max_length=200)


class Book(models.Model):
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
    title = models.CharField(max_length=300)


class Contract(models.Model):
    publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE)
    book = models.ForeignKey(Book, on_delete=models.RESTRICT)


# book.delete()       -> RestrictedError while a contract references it
# publisher.delete()  -> removes the publisher, its books and its contracts

go deeper

for a junior

Remember that both options refuse a delete and raise an exception, and that PROTECT is the one you meet most often.

for a middle

Explain that both are checked by Django's collector before any SQL, and that only RESTRICT lets a delete through when the rows also cascade.

for a senior

Walk through a concrete cascade where the two differ, name the exception each raises, and show how a view reports the blocking rows to the user.

for a principal

Decide which aggregates own which rows, and use RESTRICT only where deleting the owner is a legitimate clean-up while piecemeal deletes are not.

## Two ways to say no `on_delete` decides what Django does with rows that reference an object being deleted. Two of the options refuse the delete instead of changing anything: - **`PROTECT`** raises `django.db.models.ProtectedError`. - **`RESTRICT`** (added in Django 3.1) raises `django.db.models.RestrictedError`. Both exceptions subclass `django.db.IntegrityError`, both carry the offending rows (`protected_objects`, `restricted_objects`), and both are raised by Django's **deletion collector** while it walks the relations in Python, before any `DELETE` statement reaches the database. Neither is expressed in the SQL foreign key: the 6.1 database-level options are `DB_CASCADE`, `DB_SET_NULL` and `DB_SET_DEFAULT`, with no database variant of either refusal. ## Where they differ The difference only appears when a delete **cascades**. The collector starts from the object you delete and follows every `CASCADE` relation, gathering rows to remove. - With `PROTECT`, when the walk reaches rows that protect something in the delete set, the collector gives up with `ProtectedError`. It does not matter whether that protecting row would itself be deleted by another cascade. - With `RESTRICT`, the collector notes the restricted rows and keeps walking. At the end it removes from that list every row already scheduled for deletion through a cascade. Only if anything is left does it raise `RestrictedError`. ## A catalog walk-through A publisher signs a contract per book. The contract belongs to the publisher, and it must not be orphaned by deleting its book on its own: | Action | `Contract.book = RESTRICT` | `Contract.book = PROTECT` | |---|---|---| | `book.delete()` while a contract exists | `RestrictedError` | `ProtectedError` | | `publisher.delete()` (cascades to books and contracts) | succeeds: publisher, books and contracts removed | `ProtectedError` | | `contract.delete()` | succeeds | succeeds | The publisher delete succeeds under `RESTRICT` because the contracts that restrict the books are themselves being removed through `Contract.publisher`'s `CASCADE`. That is the case `RESTRICT` exists for: a row that must not be orphaned piecemeal, but may disappear together with the aggregate that owns it. ## How the collector reaches its verdict It helps to picture the walk the collector performs for `publisher.delete()` in the table's scenario: 1. It adds the publisher to the delete set and looks at every relation pointing at `Publisher`. 2. `Book.publisher` is `CASCADE`, so it adds the publisher's books and recurses into them. 3. Looking at relations pointing at `Book`, it finds `Contract.book` with `RESTRICT` and **records** those contracts as restricted instead of failing. 4. Back at the publisher level, `Contract.publisher` is `CASCADE`, so the same contracts are added to the delete set. 5. At the end it removes from the restricted list every row that is also in the delete set. Nothing remains, so no error is raised and the deletes run in one transaction. Swap step 3 to `PROTECT` and the walk stops there with `ProtectedError`, because a protect rule is judged on its own relation and never waits to see what else is being deleted. ## Choosing between them 1. Pick **`PROTECT`** when any path to deleting the target should stop and make a person decide, for example sales records that reference a book. Deleting a whole publisher should also be blocked while sales exist. 2. Pick **`RESTRICT`** when the referencing row belongs to a larger owner and deleting that owner should clean everything up, but deleting the referenced row alone should not. 3. Pick neither when the rows are part of the target (`CASCADE`) or the link is optional (`SET_NULL`). ## Consequences to plan for - **Handle the exception.** A view that deletes should catch `ProtectedError` or `RestrictedError` and tell the user which rows block it, instead of returning a server error. - **Bulk deletes behave the same.** `QuerySet.delete()` runs the same collector, so a queryset delete that meets a protected row fails as a whole. - **Raw SQL bypasses both.** They live in Django, not in the database; a raw `DELETE` meets only the plain foreign key constraint, which rejects the delete with a database `IntegrityError` rather than these exceptions. - **Mixed rules along one chain.** If a cascade from the publisher reaches a book that a sales line protects, the whole publisher delete fails, even when the contract rule is `RESTRICT`: each rule is evaluated on its own relation. `RESTRICT` is the rarer of the two, and interviewers ask about it precisely because the difference only shows once cascades are involved.

  • If Contract.book used PROTECT instead, why would publisher.delete() fail even though the contracts cascade from the publisher?
    The collector evaluates each relation as it reaches it. Cascading from the publisher to its books, it finds contracts that protect those books and raises `ProtectedError` there; PROTECT never checks whether the protecting rows are also scheduled for deletion. Only RESTRICT defers that decision to the end of the walk.
  • Does a Django RESTRICT rule create an ON DELETE RESTRICT clause in the database?
    No. RESTRICT emulates the SQL behaviour in Python; the foreign key Django creates carries no ON DELETE action for it. A raw SQL delete therefore meets only the plain constraint, which refuses the delete through the database's own default rather than Django's RestrictedError.

PROTECT is a lock on the item itself: nobody takes it, even when clearing the whole shelf. RESTRICT is a lock that opens only when the shelf it sits on is being cleared at the same time.

saying these in an interview costs you the question

  • RESTRICT and PROTECT are two names for the same behaviour.
  • RESTRICT adds ON DELETE RESTRICT to the database foreign key.
  • PROTECT allows the delete when the protecting rows cascade too.
  • RESTRICT deletes the referencing rows when the target goes away.
  • ProtectedError is raised after the database has already deleted rows.