skip to content

Relations & Delete Rules

ForeignKey, ManyToManyField and OneToOneField with on_delete rules, related_name and through models. Interviewers ask what a delete cascades to and whether Python or the database enforces it.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

6

In a Django model, what does a ForeignKey's on_delete argument decide, and how do you choose a value for each relation?

level: juniorimportance: must knowfreq 74%

answer

  1. what happens to the rows pointing here
  2. required argument since Django 2.0
  3. delete, block, or rewrite the key
  4. SET_NULL needs null=True

basics

~20 s

on_delete tells Django what to do with rows that reference an object when that object is deleted: delete them too (CASCADE), refuse the delete (PROTECT, RESTRICT), or rewrite their key (SET_NULL, SET_DEFAULT, SET()). It is required on every ForeignKey and OneToOneField.

solid answer

~40 s

`on_delete` is a required argument of `ForeignKey` and `OneToOneField`: when the referenced row is deleted, it decides the fate of the rows pointing at it. `CASCADE` deletes them too, `PROTECT` and `RESTRICT` refuse the delete with an exception, `SET_NULL` (needs `null=True`), `SET_DEFAULT` (needs a `default`) and `SET(value_or_callable)` rewrite the key, and `DO_NOTHING` leaves it to the database. Django 6.1 adds `DB_CASCADE`, `DB_SET_NULL` and `DB_SET_DEFAULT`, which put the rule into the SQL constraint. I choose by ownership: a row that cannot exist without its parent, like a book's co-authorship row, cascades; a reference whose loss would erase history, like a book's publisher, is protected; an optional link, like a book's series, is set to null.

code

python · 22 lines
python
from django.db import models


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


class Book(models.Model):
    title = models.CharField(max_length=300)
    publisher = models.ForeignKey(Publisher, on_delete=models.PROTECT)
    series = models.ForeignKey(
        "catalog.Series",
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )


class Authorship(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    author = models.ForeignKey("catalog.Author", on_delete=models.PROTECT)
    position = models.PositiveSmallIntegerField()

go deeper

for a junior

Recall that on_delete is required, name CASCADE, PROTECT and SET_NULL, and say that SET_NULL needs null=True on the field.

for a middle

Explain the choice per relation by ownership, the exceptions PROTECT and RESTRICT raise, and why SET() should receive a callable.

for a senior

Show that you review on_delete as a business rule: which deletions destroy history, how the user learns a delete was blocked, and what a mass cascade would remove.

for a principal

Frame delete rules as a team convention: default to protecting historical records, require a stated reason for CASCADE on anything user-facing, and decide where soft deletion replaces hard deletes.

## What on_delete is for A **foreign key** is a column holding another row's key. In Django you declare it with `models.ForeignKey(Target, on_delete=...)`. When the target row is deleted, every row pointing at it would be left holding a key that no longer exists. `on_delete` is the rule Django applies to those **referencing rows** at that moment. Since Django 2.0 the argument is **required**: `ForeignKey(Publisher)` without it fails at import time with a `TypeError`, because `on_delete` is the second positional parameter of `ForeignKey.__init__`. The same holds for `OneToOneField`. Older code that relied on an implicit cascade had to state it explicitly. ## The options All of them are imported from `django.db.models`: | Option | What happens to referencing rows | Requirement | |---|---|---| | `CASCADE` | deleted along with the target | none | | `PROTECT` | delete refused with `ProtectedError` | none | | `RESTRICT` | delete refused with `RestrictedError`, unless the rows are also being deleted through a cascade in the same operation | none | | `SET_NULL` | key set to `NULL` | `null=True` on the field | | `SET_DEFAULT` | key set to the field's `default` | a `default` | | `SET(value)` | key set to a value or a callable's result | none | | `DO_NOTHING` | Django does nothing; the database decides | a database rule, or an `IntegrityError` | | `DB_CASCADE`, `DB_SET_NULL`, `DB_SET_DEFAULT` (6.1) | same effects, but carried out by the database's `ON DELETE` clause | a migration; `DB_SET_DEFAULT` needs `db_default` | `ProtectedError` and `RestrictedError` are both subclasses of `django.db.IntegrityError`, so a view can catch them explicitly and show the user what blocks the delete (`error.protected_objects`). Django's **system checks** catch the most common mismatches at startup: `SET_NULL` on a field without `null=True` is error `fields.E320`, and `SET_DEFAULT` without a default is `fields.E321`. ## Choosing by ownership Ask one question per relation: *does the referencing row mean anything once the target is gone?* 1. **The row is part of the target** — it has no meaning on its own. Use `CASCADE`. In a publisher's catalog, the `Authorship` row linking a book to a co-author exists only for that book. 2. **The row records history you must keep** — sales lines, contracts, royalty statements. Use `PROTECT` (or `RESTRICT`), so deleting the target is refused and someone has to decide what to do first. A book's `publisher` is a typical case: deleting a publisher should not silently delete its back catalog. 3. **The link is optional** — the row stands on its own without it. Use `SET_NULL` with `null=True` (and usually `blank=True` so forms accept an empty value). A book's `series` fits. 4. **The link should move to a placeholder** — use `SET(callable)`, for example re-pointing to a sentinel "Unknown editor" row. Pass a callable rather than a queried object so no query runs when `models.py` is imported. ## Where people go wrong - Putting `CASCADE` everywhere because it is the shortest to type: one admin click on a publisher can then remove thousands of books and their co-authorship rows. - Using `SET_NULL` without `null=True`: the check framework reports it before the code ever runs. - Treating `DO_NOTHING` as "keep the rows": with a normal foreign key constraint the database refuses the delete instead, so the caller gets an `IntegrityError`. - Forgetting that Django applies the Python-level options when you delete through the ORM (`instance.delete()` or `QuerySet.delete()`); a raw SQL `DELETE` bypasses them. ## Reading the rules in a code review When reviewing a model, read each `on_delete` as a sentence about the business and check it against what the team expects: - **`CASCADE` on a row that carries money or history** (sales lines, royalty statements, contracts) is almost always a bug; a delete upstream would erase records that accounting needs. - **`PROTECT` on a purely dependent row** (a book's cover image record, a co-authorship link) makes deletes needlessly painful: every book delete would first require deleting its links by hand. - **`SET_NULL` on a relation the application treats as required** leaves rows the rest of the code cannot handle, such as a book with no publisher that a report later divides by. - **Deleting through the ORM** runs these rules for both `instance.delete()` and `QuerySet.delete()`; both return the number of rows deleted and a per-model breakdown, which is a quick way to see in a shell how far a cascade reaches before running it for real inside a transaction you roll back. ## A catalog example The model in the code example below uses three different rules on purpose. The publisher is protected, the series is optional, and co-authorship rows cascade from the book. The string reference `"catalog.Series"` shows the lazy form: an `app_label.ModelName` string lets a model point at another app's model without importing it, which also breaks circular imports. Choosing `on_delete` is a data-modelling decision, not boilerplate. A reviewer reading a model should be able to tell, from these arguments alone, which deletions the business allows.

  • How do you point a Django ForeignKey at a model in another app that itself imports this module?
    Use a lazy string reference, `ForeignKey("catalog.Author", on_delete=...)`, in `app_label.ModelName` form. Django resolves it once the app registry is ready, so neither module imports the other and the circular import disappears. Inside the same app the bare `"Author"` also works, and `"self"` gives a recursive relation.
  • How should a Django view handle a PROTECT rule blocking a delete?
    Catch `django.db.models.ProtectedError` around the delete. Its `protected_objects` attribute holds the rows that block it, so the view can tell the user, for example, which books still reference the publisher, instead of returning a 500. The same pattern applies to `RestrictedError` and its `restricted_objects`.

saying these in an interview costs you the question

  • Omitting on_delete is fine because it defaults to CASCADE.
  • SET_NULL works on any ForeignKey without null=True.
  • DO_NOTHING keeps the referencing rows and deletes the target.
  • PROTECT deletes the target and then raises an error afterwards.
  • CASCADE is the safe default for every relation.
open as a page

In Django, when does a ManyToManyField need an explicit through model, and how does it change adding and removing related rows?

level: middleimportance: should knowfreq 52%

basics

~20 s

Use a through model when the link itself carries data, such as an author's position and royalty share on a book. add(), create() and set() still work, but required link fields must come via through_defaults, and pair uniqueness is yours to declare.

open as a page

In Django, when do you choose OneToOneField over ForeignKey, and what does its reverse accessor do when no related row exists?

level: middleimportance: should knowfreq 48%

basics

~20 s

OneToOneField is a unique foreign key whose reverse side returns a single object instead of a manager. Use it when each row has at most one partner. When no partner exists, the reverse accessor raises RelatedObjectDoesNotExist.

open as a page

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%

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.

open as a page

In Django, is on_delete=CASCADE enforced by Python or by the database, and what changes when you switch to Django 6.1's DB_CASCADE?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Classic CASCADE runs in Python: Django's deletion collector finds the related rows and deletes them itself, sending delete signals. Django 6.1's DB_CASCADE writes ON DELETE CASCADE into the foreign key instead, so the database deletes children without Django fetching them or sending signals.

open as a page