Relationships
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Django 6.1 (6.1.1 on PyPI; 6.1.2 is scheduled for 2026-10-06), Python 3.12-3.14 and
PostgreSQL 15+, checked on 2026-10-03; where Django 5.2 LTS behaves differently the text says so. Every listing was run
in a scratch project on Python 3.13.16 and Django 6.1.1 with SQLite: manage.py check, makemigrations, sqlmigrate
and a test suite of 25 tests (manage.py test) pass. SQLite is the only database that was available, so the
SQL fragments shown are SQLite’s; the ON DELETE clauses and the behaviour of the DB-level options are the same idea on
PostgreSQL, but run them against your production database before relying on them.
A relational database stores a relationship as a column that refers to another row’s key, and Django models that column
as a field whose Python value is a whole related object. Three field types cover the three classic cardinalities — ForeignKey (many-to-one), ManyToManyField (many-to-many) and OneToOneField (one-to-one) — and each one gives you
three things at once: a column or table in the schema, an attribute on the model instance that loads the related object
(or a manager for many of them), and a query syntax that spans the join (bookpublishername). This page covers
all three, how the reverse side is named and used, what happens on deletion (on_delete, including the options new in
Django 6.1), how a relationship interacts with the async ORM and with the new fetch modes, and the pitfalls that cause
most production bugs. The running example is the Bookshelf: publishers, authors, books, reviews, categories and a
profile per user.
The Bookshelf entity-relationship diagram
The figure shows the relationships used in the rest of the page. Crow’s-foot notation reads "one publisher has zero or
more books"; AUTHORSHIP is the explicit join table (a through model) between BOOK and AUTHOR, and
BOOK_CATEGORY is the join table Django generates for a plain ManyToManyField.
Where relationships sit in Django
Relationships are declared on models, so they are processed in the app loading stages described in
Project Structure & Apps: Django imports each app’s models module in
stage 2, and a relationship that names its target as a string ("Publisher", "self", "shop.Product") is
resolved lazily once both models are registered. This is why you can reference a model that is defined later in the
file or in another app without a circular import. The system checks (manage.py check, which runserver and migrate
also run) then validate the relationships — a missing on_delete is a TypeError at import time, and clashing reverse
names, an ambiguous through model or on_delete=SET_NULL on a non-nullable field are reported as errors such as
fields.E304, fields.E305, fields.E335 and fields.E320 before a request is ever served.
At request time a relationship is just ORM access: the URLconf picks a view
(Request-Response Cycle), the view runs queries through
QuerySets, and touching book.publisher or book.authors.all() issues SQL
immediately and synchronously unless the data was loaded with the original query. Deletion is the other moment
relationships act: Model.delete() and QuerySet.delete() run the collector, which walks the reverse
relationships, applies each on_delete rule and emits one batch of DELETE/UPDATE statements — or, for the DB-level
options, leaves the work to the database.
Models used on this page
The listing is the complete bookshelf/models.py of the scratch project (a few models used only for later
sections, Reader/Club/Membership and Isbn/Edition, appear where they are discussed). The user is referenced
through settings.AUTH_USER_MODEL, never imported directly, so the code keeps working with a custom user model
(see Custom User Model & Backends).
from django.conf import settings
from django.db import models
class Publisher(models.Model):
name = models.CharField(max_length=200, unique=True)
def __str__(self):
return self.name
class Author(models.Model):
name = models.CharField(max_length=200)
# Self-relation: symmetrical by default, so no author_set accessor is added.
collaborators = models.ManyToManyField("self")
# Asymmetric self-relation: one direction, a reverse accessor exists.
mentor = models.ForeignKey(
"self",
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name="mentees",
)
def __str__(self):
return self.name
class Category(models.Model):
name = models.CharField(max_length=100)
parent = models.ForeignKey(
"self",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="children",
)
def __str__(self):
return self.name
class Book(models.Model):
title = models.CharField(max_length=300)
publisher = models.ForeignKey(
Publisher, on_delete=models.PROTECT, related_name="books"
)
authors = models.ManyToManyField(
Author, through="Authorship", related_name="books"
)
categories = models.ManyToManyField(Category, blank=True, related_name="books")
def __str__(self):
return self.title
class Authorship(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name="authorships")
author = models.ForeignKey(
Author, on_delete=models.CASCADE, related_name="authorships"
)
position = models.PositiveSmallIntegerField(default=1)
role = models.CharField(max_length=30, default="author")
class Meta:
constraints = [
models.UniqueConstraint(
fields=["book", "author"], name="unique_author_per_book"
)
]
ordering = ["position"]
class Review(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name="reviews")
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name="reviews",
related_query_name="review",
)
rating = models.PositiveSmallIntegerField()
body = models.TextField(blank=True)
class Profile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile"
)
bio = models.TextField(blank=True)
ForeignKey: many-to-one
ForeignKey(to, on_delete, *options) stores a reference from the model that declares it (the *child, here Book)
to exactly one row of another model (the parent, Publisher). Both positional arguments are required: the target
model — a class or a lazy string — and on_delete, which says what happens to the child when the parent is deleted.
Database representation
Django appends _id to the field name to create the column (publisher_id), creates a foreign-key constraint, and
creates an index on the column automatically. sqlmigrate shows exactly what the schema editor will run:
python manage.py sqlmigrate bookshelf 0001 | grep 'CREATE TABLE "bookshelf_authorship"'
CREATE TABLE "bookshelf_authorship" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "position" smallint unsigned NOT NULL CHECK ("position" >= 0), "role" varchar(30) NOT NULL, "author_id" bigint NOT NULL REFERENCES "bookshelf_author" ("id") DEFERRABLE INITIALLY DEFERRED);
-- ...later in the same migration, the table is rebuilt to add book_id:
CREATE INDEX "bookshelf_authorship_author_id_d1c8cb8c" ON "bookshelf_authorship" ("author_id");
CREATE INDEX "bookshelf_authorship_book_id_da43ec59" ON "bookshelf_authorship" ("book_id");
Two details matter. First, the constraint is DEFERRABLE INITIALLY DEFERRED on databases that support it, so the check
happens at commit time, not statement by statement (see
Transactions, Raw SQL & Multiple Databases). Second,
a Python-level on_delete (CASCADE, PROTECT, SET_NULL, …) emits no ON DELETE clause at all: the rule lives
in Django, not in the schema. Only the DB_* options in Django 6.1 write the clause (see
the section below).
The automatic index can be switched off with db_index=False if the column exists for consistency only and you will
create a better partial or multi-column index (see
Constraints, Indexes & Composite Keys).
db_constraint=False removes the database constraint itself; the official documentation warns this "can be very bad
for data integrity" and lists legacy invalid data and sharding as the only good reasons. With it set, accessing a
dangling reference raises the target’s DoesNotExist.
Using the forward and reverse sides
Reading book.publisher runs a query the first time and caches the instance on book; book.publisher_id is the
raw value and never queries. The reverse side is a related manager named after related_name (publisher.books).
Queries span the relationship with double underscores in both directions.
book = Book.objects.get(pk=1)
book.publisher # one query on first access, cached afterwards
book.publisher_id # no query: the raw column value
publisher.books.all() # reverse side: a RelatedManager, i.e. a QuerySet of its books
Book.objects.filter(publisher__name="Packt") # forward join
Publisher.objects.filter(books__title__startswith="Dj") # reverse join, uses related_name
The scratch test asserts that two accesses to book.publisher cost one query and that select_related("publisher")
folds the load into the original query:
book = Book.objects.get(pk=self.book.pk)
with CaptureQueriesContext(connection) as ctx:
book.publisher
book.publisher
assert len(ctx) == 1
with CaptureQueriesContext(connection) as ctx:
Book.objects.select_related("publisher").get(pk=book.pk).publisher
assert len(ctx) == 1 # one JOIN query, no second query for publisher
Every ForeignKey option
| Option | What it does |
|---|---|
|
Required. What happens to this row when the referenced row is deleted. See every |
|
Name of the reverse accessor on the target. Default |
|
Name of the reverse filter ( |
|
Column of the target to reference instead of the primary key. It must be |
|
Restricts the choices shown in a |
|
|
|
The common field options: index (on by default for relations), column name, and, with |
|
Controls how migrations record a reference to a swappable model such as the user model: by |
|
Field options. |
The Bookshelf uses to_field and related_name="+" on its Edition model, which hangs an ISBN off a book through the
ISBN string instead of its numeric id and declines to create a book.edition_set:
class Isbn(models.Model):
code = models.CharField(max_length=13, unique=True)
class Edition(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name="+")
isbn = models.ForeignKey(
Isbn, to_field="code", on_delete=models.PROTECT, db_column="isbn_code"
)
isbn = Isbn.objects.create(code="9781805125457")
ed = Edition.objects.create(book=book, isbn=isbn)
ed.isbn_id # '9781805125457' -- the to_field value, not an integer
Edition._meta.get_field("isbn").column # 'isbn_code'
hasattr(book, "edition_set") # False: related_name="+"
Every on_delete option
When the referenced row is deleted, each row that points at it must be dealt with. on_delete chooses the rule; the
values are imported from django.db.models. Before Django 2.0 the argument was optional and defaulted to CASCADE;
since 2.0 it is required in models and migrations, so code, tutorials and migrations written for 1.x that omit it raise
TypeError: ForeignKey.init() missing 1 required positional argument: 'on_delete' and cannot be used as-is.
| Option | Who acts | Behaviour |
|---|---|---|
|
Django |
Deletes the referring rows too, emulating |
|
Django |
Refuses the deletion by raising |
|
Django |
Raises |
|
Django |
Sets the key to |
|
Django |
Sets the key to the field’s Python |
|
Django |
Sets the key to the value, or to the result of calling the callable; use a callable so no query runs at import time. |
|
Nobody |
No action. On a database that enforces integrity this causes an |
|
Database |
Database-level |
|
Database |
Database-level |
|
Database |
Database-level |
Source: https://docs.djangoproject.com/en/6.1/ref/models/fields/#django.db.models.ForeignKey.on_delete
Python-level options in action
The Bookshelf picks each rule for a reason: reviews and authorships are meaningless without their book (CASCADE);
a publisher with books must not vanish by accident (PROTECT); a deleted user should leave their reviews behind
(SET_NULL, hence null=True); and when an author’s mentor is deleted, the mentee remains (SET_NULL on the
self-reference). The scratch tests exercise each of them:
# PROTECT
try:
publisher.delete()
except ProtectedError:
pass # the publisher still has books
# SET_NULL on Review.user
user.delete()
review.refresh_from_db()
review.user # None: the review survives, anonymised
# CASCADE: deleting a book removes its authorships and reviews
book.delete()
# (4, {'bookshelf.Authorship': 2, 'bookshelf.Review': 1, 'bookshelf.Book': 1})
delete() returns a tuple: the total number of rows deleted and a per-model breakdown, which is the quickest way to
see what a cascade will take with it. A RESTRICT example from the official documentation is the case where the
Python level cannot be replaced by PROTECT: when a Song references both an Album (RESTRICT) and the album’s
Artist (CASCADE), deleting the artist succeeds (the song goes away in the same operation), while deleting just the
album raises RestrictedError. PROTECT would refuse both.
Use SET() with a callable for "reassign to a sentinel" policies:
from django.conf import settings
from django.contrib.auth import get_user_model
from django.db import models
def get_sentinel_user():
return get_user_model().objects.get_or_create(username="deleted")[0]
class Comment(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.SET(get_sentinel_user)
)
Database-level options (Django 6.1)
The Python-level options make Django load the affected rows, send signals and issue the statements itself, which costs
queries and memory proportional to the number of children. Django 6.1 adds three options that hand the work to the
database through the SQL ON DELETE clause: DB_CASCADE, DB_SET_NULL and DB_SET_DEFAULT. They are more
efficient because nothing needs to be fetched, at the price of the Python-level behaviour: with DB_CASCADE the
pre_delete and post_delete signals are not sent for the cascaded rows, and their delete() is not
called.
The scratch app ondb defines one parent and three children, one per option. All of them are DB-level because the
two kinds cannot be mixed (see the rules below):
from django.db import models
class Shelf(models.Model):
name = models.CharField(max_length=100)
class Copy(models.Model):
shelf = models.ForeignKey(Shelf, on_delete=models.DB_CASCADE, related_name="copies")
title = models.CharField(max_length=200)
class Loan(models.Model):
shelf = models.ForeignKey(
Shelf, on_delete=models.DB_SET_NULL, null=True, related_name="loans"
)
class Label(models.Model):
shelf = models.ForeignKey(
Shelf, on_delete=models.DB_SET_DEFAULT, db_default=1, related_name="labels"
)
sqlmigrate ondb 0001 shows the difference from the Python-level options — the column now carries the clause:
CREATE TABLE "ondb_loan" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "shelf_id" bigint NULL REFERENCES "ondb_shelf" ("id") ON DELETE SET NULL DEFERRABLE INITIALLY DEFERRED);
CREATE TABLE "ondb_label" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "shelf_id" bigint DEFAULT 1 NOT NULL REFERENCES "ondb_shelf" ("id") ON DELETE SET DEFAULT DEFERRABLE INITIALLY DEFERRED);
CREATE TABLE "ondb_copy" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" varchar(200) NOT NULL, "shelf_id" bigint NOT NULL REFERENCES "ondb_shelf" ("id") ON DELETE CASCADE DEFERRABLE INITIALLY DEFERRED);
Deleting a shelf then needs one statement from Django; the database does the rest, and a signal receiver connected to
pre_delete for Copy never fires:
total, per_model = shelf.delete()
# (1, {'ondb.Shelf': 1}) <- only the shelf: Django did not count or load the children
loan.refresh_from_db(); loan.shelf_id # None (DB_SET_NULL)
label.refresh_from_db(); label.shelf_id # 1 (DB_SET_DEFAULT, the db_default)
Copy.objects.exists() # False (DB_CASCADE)
The rules to remember, from the official documentation and the checks observed in the scratch project:
-
The DB variants "cannot be mixed with Python variants (other than
DO_NOTHING) in the same model and in models related to each other". System checks enforce it:models.E050flags a model whose related fields use both levels, andfields.E323flags a field whose referenced (or auto-created intermediary) model uses the other level; see System check framework: fields. -
DB_SET_NULLon a non-null field isfields.E320;DB_SET_DEFAULTwithoutdb_defaultisfields.E322. -
DB_SET_DEFAULTis not supported on MySQL and MariaDB (fields.E324reports an option the database lacks); the referenced default row must exist or the update will itself violate the constraint. -
The
delete()return value does not count rows removed by the database, so code that reads the per-model counts to audit deletions undercounts. -
Because no signal fires, receivers that clean up files, caches or search indexes on
post_delete(see Signals) must be replaced with another mechanism, or the model should keep the Python-levelCASCADE. -
A database-level rule applies to every client of the database: a script or a
DELETEtyped inpsqlfollows it as well, which is a benefit (consistency) and a risk (the rule cannot be bypassed from Django).
Source: https://docs.djangoproject.com/en/6.1/ref/models/fields/#django.db.models.DB_CASCADE and https://docs.djangoproject.com/en/6.1/releases/6.1/#database-level-delete-options-for-foreignkey-on-delete
A simple decision guide for the Bookshelf-style choices:
| Situation | Choose |
|---|---|
Child cannot exist without the parent, few children, receivers need signals |
|
Child cannot exist without the parent, very many children, no |
|
Parent must not be deleted while children exist |
|
Child should outlive the parent and keep a hole |
|
Child should fall back to a known row |
|
ManyToManyField: many-to-many
ManyToManyField(to, *options) relates many rows on each side. It does not create a column; Django creates an
*intermediary table named from the declaring model’s table and the field (bookshelf_book_categories), with a primary
key, one foreign key to each side and a unique constraint on the pair. The Bookshelf’s tagging is a plain
many-to-many: a book belongs to several categories and a category groups several books.
class Book(models.Model):
categories = models.ManyToManyField(Category, blank=True, related_name="books")
You can reach the table that Django generated when you need to query or bulk-insert it directly:
Through = Book.categories.through
[f.name for f in Through._meta.fields]
# ['id', 'book', 'category']
book.categories.add(category)
Through.objects.count() # 1
add, remove, set, clear, create and the other manager methods change the table immediately; there is no
need to call save() on either side. In a ModelForm saved with commit=False you must call form.save_m2m() yourself
after saving the instance, because the table cannot be written before the row exists.
Source: https://docs.djangoproject.com/en/6.1/ref/models/fields/#manytomanyfield and https://docs.djangoproject.com/en/6.1/topics/db/examples/many_to_many/
ManyToManyField options
| Option | What it does |
|---|---|
|
Same as on |
|
Only for |
|
The model (class or lazy string) used as the intermediary table. See below. |
|
A 2-tuple |
|
Name of the generated join table. |
|
|
|
As on |
ManyToManyField does not support validators, and null has no effect, since a relationship cannot be required at the
database level. If you need "at least one", enforce it in a form, a serializer or a service function.
Extra data on the relationship: through
Whenever the relationship itself has attributes — here the author’s position and role on a book — declare the
intermediary model yourself and point at it with through. The through model must contain exactly one foreign key to the source model and one to the target (or
you pass through_fields), and you should add a UniqueConstraint on the pair if duplicates must not exist, because the
automatic table’s constraint disappears when you take over the table.
class Book(models.Model):
authors = models.ManyToManyField(Author, through="Authorship", related_name="books")
class Authorship(models.Model):
book = models.ForeignKey(Book, on_delete=models.CASCADE, related_name="authorships")
author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="authorships")
position = models.PositiveSmallIntegerField(default=1)
role = models.CharField(max_length=30, default="author")
class Meta:
constraints = [
models.UniqueConstraint(fields=["book", "author"], name="unique_author_per_book")
]
ordering = ["position"]
You can create relationships by creating through instances, or — since Django 2.2 — with the manager methods
add(), create(), set() (and get_or_create()/update_or_create()) as long as through_defaults supplies the
through model’s required fields. Before 2.2 the manager methods were refused on many-to-many relations with a custom
through model, so older advice to "always create the through row by hand" is outdated, though still valid.
Authorship.objects.create(book=book, author=ann, position=1)
Authorship.objects.create(book=book, author=bob, position=2, role="editor")
[a.name for a in book.authors.all()] # ['Ann', 'Bob']
book.authors.add(carl, through_defaults={"position": 3, "role": "translator"})
book.authors.remove(carl) # deletes the Authorship row(s)
book.authors.set([bob], through_defaults={"position": 1})
book.authors.count() # 1
ann.books.count() # 0, the reverse side is a manager too
# Query on the through model's attributes through the relation
Book.objects.filter(authorships__author=bob, authorships__role="editor")
# A duplicate pair violates the unique constraint
Authorship.objects.create(book=book, author=ann) # IntegrityError
Three behaviours deserve attention. remove() on a through relation deletes every through row for that pair if the
table does not enforce uniqueness, and clear() deletes the through rows (not the related objects). The ordering of
book.authors.all() is that of the Author model (unspecified unless Author.Meta.ordering is set), not of
Authorship.position; to order by position, query the through model
(book.authorships.select_related("author"), which uses Authorship.Meta.ordering) or order explicitly with
order_by("authorships__position").
And add() on an existing relation does not duplicate it but still sends the m2m_changed signal.
Ambiguous through models: through_fields
When the through model has two foreign keys to the same participant — a reading club’s membership records who invited the member — Django cannot guess which key is the member, so name them.
class Reader(models.Model):
name = models.CharField(max_length=50)
class Club(models.Model):
name = models.CharField(max_length=128)
members = models.ManyToManyField(
Reader,
through="Membership",
through_fields=("club", "reader"),
related_name="clubs",
)
class Membership(models.Model):
club = models.ForeignKey(Club, on_delete=models.CASCADE)
reader = models.ForeignKey(Reader, on_delete=models.CASCADE)
inviter = models.ForeignKey(
Reader, on_delete=models.CASCADE, related_name="membership_invites"
)
invite_reason = models.CharField(max_length=64)
club.members.add(a, through_defaults={"inviter": b, "invite_reason": "fan"})
list(club.members.all()) # [a]
list(b.membership_invites.all()) # the Membership rows b invited
Without through_fields the scratch system check reports fields.E335: the model is used as an intermediate model
but "has more than one foreign key" to the participant. Note that through_fields is (source, target): the first
name is the foreign key to the model that declares the field.
OneToOneField: one-to-one
OneToOneField(to, on_delete, parent_link=False, **options) is a ForeignKey with unique=True whose reverse side
returns a single object instead of a manager. It is the standard way to extend a model you do not own — the Bookshelf’s
Profile extends the user — and the mechanism behind multi-table inheritance (see
Model Inheritance & the Meta API), where Django adds an implicit
one-to-one link from child to parent and parent_link=True lets you declare that link yourself.
class Profile(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="profile"
)
bio = models.TextField(blank=True)
The default reverse accessor is the lowercase model name (user.profile); it raises
RelatedObjectDoesNotExist — a subclass of the target’s DoesNotExist — when no row exists, so the check "does
this user have a profile?" is a try/except, not a None test.
user = User.objects.create_user("zoe", password="...") # use a real, non-hardcoded value in your code
try:
user.profile
except User.profile.RelatedObjectDoesNotExist:
... # no profile yet
Profile.objects.create(user=user, bio="hi")
user = User.objects.get(pk=user.pk) # reload: the miss may be cached
user.profile.bio # 'hi'
Profile.objects.create(user=user) # IntegrityError: one profile per user
Self-relations and lazy references
A model can relate to itself with "self". A ForeignKey("self") models a hierarchy — the Bookshelf’s Category.parent
(with related_name="children") and an author’s mentor — and ManyToManyField("self") models peers.
root = Category.objects.create(name="Tech")
kid = Category.objects.create(name="Python", parent=root)
list(root.children.all()) # [kid]
root.delete() # CASCADE: removes the subtree
Category.objects.exists() # False
ann.collaborators.add(bob)
list(bob.collaborators.all()) # [ann]: symmetrical, so bob sees ann without a second add
hasattr(bob, "author_set") # False: no reverse accessor on a symmetrical self-relation
Set symmetrical=False for directional relations ("follows"), which also creates the reverse accessor. With an
intermediate model on a self-relation the first foreign key is the source and the second the target unless
through_fields says otherwise, and you must give at least one related_name because both keys would otherwise
produce the same reverse accessor.
A relationship argument can be a class or a string, and the string comes in three forms. "self" is recursive;
"Manufacturer" is relative, resolved against the app of the model (or, on an abstract model, the concrete subclass
that inherits the field, which is how reusable abstract models point at whatever Manufacturer the including app
defines); and "thirdpartyapp.Manufacturer" is absolute, always the same model, which also breaks circular
imports between two apps.
class Car(models.Model):
manufacturer = models.ForeignKey("Manufacturer", on_delete=models.CASCADE) # relative
class Listing(models.Model):
product = models.ForeignKey("shop.Product", on_delete=models.CASCADE) # absolute: no import
related_name, related_query_name and clashes
Every relationship gets a reverse accessor on the target model and a reverse query name for filters. By default
they derive from the declaring model (book_set, book), which is rarely the name you want in your own code.
related_name sets the accessor and, unless overridden, the query name; related_query_name overrides only the query
name; "+" suppresses the reverse relation entirely (no accessor, no reverse filter).
class Review(models.Model):
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True,
related_name="reviews", # user.reviews
related_query_name="review", # User.objects.filter(review__rating__gte=4)
)
user.reviews.count() # reverse manager, plural
User.objects.filter(review__rating__gte=4).count() # reverse filter, singular
Two foreign keys from one model to the same target, with no related_name, create two reverse accessors with the same
name; the system check reports fields.E304 (accessor clash) and fields.E305 (query-name clash) and the project does not
start. Give each key a distinct related_name. On abstract models related_name is mandatory and supports
%(class)s and %(app_label)s so each concrete subclass gets its own name; Meta.default_related_name sets the
default for a whole model. Do not name a reverse accessor after a field or method of the target model.
Related managers
The reverse side of a ForeignKey, and both sides of a ManyToManyField, are related managers: a manager (see
Managers & Custom QuerySets) already filtered to the owner, so
publisher.books.filter(title__icontains="django"), .order_by(), .count(), .exists() and the rest of the
QuerySet API work. In addition they carry methods that change the relationship. All of them have an a-prefixed async
twin.
| Method | Behaviour |
|---|---|
|
Associates existing objects. On a |
|
Creates, saves and associates a new object in one step; you do not pass the relating field. |
|
Disassociates. On a |
|
Disassociates everything; same |
|
Replaces the set: removes what is missing and adds what is new (or, with |
The Bookshelf’s Review.book is not nullable, so book.reviews has no remove(); Review.user is, so user.reviews
does. The scratch test confirms both, and that add() moves a review between books:
review = book.reviews.create(rating=3) # creates and links; no book= argument
other.reviews.add(review) # UPDATE: review now belongs to `other`
hasattr(other.reviews, "remove") # False: Review.book is null=False
hasattr(user.reviews, "remove") # True: Review.user is null=True
None of these methods needs save() on either end. If you use prefetch_related(), add, remove, clear and set
clear the prefetched cache, so re-read the relation afterwards. m2m_changed is the signal to listen to for
many-to-many changes, since neither save() nor delete() is called on through rows.
Querying across relationships
Lookups cross relations with __ in either direction, forward through the field name and backward through the reverse
query name, as many hops as necessary, and the database joins for you. Spanning a multi-valued relation in one filter() call
requires both conditions to hold for the same related row, while two chained filter() calls may match different
rows — the single most surprising rule for newcomers (see
Making Queries for the full treatment, and
Aggregation, Expressions & Functions for counting related rows
with annotate(Count(…))).
Book.objects.filter(authorships__author=bob, authorships__role="editor") # same Authorship row
Book.objects.annotate(n=Count("authors")).get().n # 2
Author.objects.filter(~Q(books=None)).distinct() # authors with at least one book
Loading related objects efficiently
Reading book.publisher for each book of a list is the classic N+1 problem: one query for the list plus one per
row. Django has had two tools for it for a long time — select_related() (a SQL JOIN for forward single-valued
relations) and prefetch_related() (a second query, joined in Python, for many-valued relations) — and adds a third in
6.1, fetch modes. The scratch test counts queries for three books, each with a publisher and an author:
def run(qs):
with CaptureQueriesContext(connection) as ctx:
for b in qs:
b.publisher.name
[x.name for x in b.authors.all()]
return len(ctx)
run(Book.objects.all()) # 7
run(Book.objects.select_related("publisher")) # 4
run(Book.objects.select_related("publisher").prefetch_related("authors")) # 2
Fetch modes (new in 6.1) change what happens when a field that was not loaded is accessed. FETCH_ONE is the default
(one query per instance), FETCH_PEERS loads the missing field for the instance and all instances of the same
QuerySet in one batch, and FETCH_RAISE raises FieldFetchBlocked so accidental queries fail loudly in
performance-critical code. They apply to forward ForeignKey, OneToOneField and its reverse accessor, deferred
fields and generic relations; they do not change the queries issued by related managers, so many-valued relations
still need prefetch_related():
from django.core.exceptions import FieldFetchBlocked
from django.db import models
Book.objects.all() # 4 queries for 3 books (1 + 3)
Book.objects.fetch_mode(models.FETCH_PEERS) # 2 queries: books, then all publishers at once
Book.objects.fetch_mode(models.FETCH_RAISE) # FieldFetchBlocked: "Fetching of Book.publisher blocked."
for b in Book.objects.fetch_mode(models.FETCH_PEERS):
list(b.authors.all()) # still 1 query per book: related managers are unaffected
The complete treatment, including making a mode the default through a custom manager, is on Query Optimization & Fetch Modes.
Sync and async behaviour
Relationships are the place where the async ORM bites. The related-manager methods (aadd, acreate, aremove,
aclear, aset, since Django 4.2) and the QuerySet methods (aget, acount, async for, since 4.1) are awaitable. But a lazy
attribute — book.publisher on an instance whose publisher was not loaded — is a plain Python attribute that must run
a blocking query, so in an async context it raises SynchronousOnlyOperation. Load the relation with the original
query (select_related, prefetch_related, or FETCH_PEERS in sync code) or fetch it explicitly with aget.
from asgiref.sync import async_to_sync
from django.core.exceptions import SynchronousOnlyOperation
async def reads(book_pk):
book = await Book.objects.aget(pk=book_pk)
pub = await Publisher.objects.aget(pk=book.publisher_id) # explicit async fetch
authors = [a async for a in book.authors.all()] # async iteration of a related manager
await book.reviews.acreate(rating=2) # async related-manager method
return pub.name, len(authors), await book.reviews.acount()
async def lazy_fk(book_pk):
book = await Book.objects.aget(pk=book_pk)
try:
book.publisher # lazy forward access
except SynchronousOnlyOperation:
return "SynchronousOnlyOperation"
async def loaded_fk(book_pk):
book = await Book.objects.select_related("publisher").aget(pk=book_pk)
return book.publisher.name # already loaded: safe
async_to_sync(reads)(book.pk) # ('Packt', 2, 1)
async_to_sync(lazy_fk)(book.pk) # 'SynchronousOnlyOperation'
async_to_sync(loaded_fk)(book.pk) # 'Packt'
The same rule applies to templates rendered in an async view: {{ review.book.title }} will fail unless book was
loaded with the review. See Async Support.
Pitfalls
-
Unsaved instances. Saving a child that points to an unsaved parent raises
ValueError: save() prohibited to prevent data loss due to unsaved related object 'publisher'. Since Django 4.1 using the reverse manager of an unsaved instance also raisesValueError('Publisher' instance needs to have a primary key value before this relationship can be used), to match the many-to-many behaviour. Both messages were captured in the scratch project. -
Chained
filter()on a multi-valued relation matches different related rows; onefilter()call with both conditions matches the same row. -
Cascades are bigger than they look. Always read the
delete()result, and review the whole graph ofon_deleterules before adding a relationship; oneCASCADEcan remove far more than intended, and a cascade does not callModel.delete()on the related rows (only the signals are sent). -
related_nameis part of your API. Renaming it needs a code change at every use; choose plural nouns for many-valued reverse sides. -
Through tables lose their automatic uniqueness. Add a
UniqueConstrainton the pair yourself. -
ForeignKeycannot point at a model with a composite primary key. Django 5.2 introducedCompositePrimaryKeyand relational fields do not support it yet; the documented workaround isForeignObjectwith explicitfrom_fieldsandto_fields, which creates no column, constraint or index and ignoreson_delete. See Constraints, Indexes & Composite Keys. -
Migrations and relationships. Adding a non-null
ForeignKeyto a table with rows needs a default or a three-step migration (nullable, backfill, then non-null); switching between Python-levelon_deleteoptions produces a migration whose SQL is a no-op (the scratch project showed-- (no-op)insqlmigrate), because the rule lives in Django, while moving to or from aDB_*option changes the constraint in the database. See Migrations. -
Swappable user model. Reference it by
settings.AUTH_USER_MODELin relationships and withget_user_model()in code that runs after app loading.
Where the books are outdated, and what changed recently
Tutorials and books written against Django 1.x and 2.x still circulate; the relationship-related differences are:
| Version | Change |
|---|---|
2.0 |
|
2.2 |
|
3.1 |
|
4.1 |
Async QuerySet interface ( |
4.2 |
Related managers gained |
5.2 LTS |
|
6.1 |
|
Two further habits from older material are worth dropping: importing django.contrib.auth.models.User directly into
foreign keys (use settings.AUTH_USER_MODEL), and assuming the default reverse name (book_set) is acceptable in a
public API (name it).
Source: https://docs.djangoproject.com/en/6.1/releases/2.0/, https://docs.djangoproject.com/en/6.1/releases/2.2/, https://docs.djangoproject.com/en/6.1/releases/3.1/, https://docs.djangoproject.com/en/6.1/releases/4.1/, https://docs.djangoproject.com/en/6.1/releases/4.2/, https://docs.djangoproject.com/en/6.1/releases/5.2/ and https://docs.djangoproject.com/en/6.1/releases/6.1/