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.

erDiagram USER ||--o| PROFILE : "has (OneToOne)" USER |o--o{ REVIEW : "writes (FK, SET_NULL)" PUBLISHER ||--o{ BOOK : "publishes (FK, PROTECT)" BOOK ||--o{ REVIEW : "receives (FK, CASCADE)" BOOK ||--o{ AUTHORSHIP : "credits (FK, CASCADE)" AUTHOR ||--o{ AUTHORSHIP : "credited in (FK, CASCADE)" BOOK ||--o{ BOOK_CATEGORY : "tagged by (implicit through)" CATEGORY ||--o{ BOOK_CATEGORY : "groups (implicit through)" CATEGORY |o--o{ CATEGORY : "parent (self FK, CASCADE)" AUTHOR |o--o{ AUTHOR : "mentor (self FK, SET_NULL)" PUBLISHER { bigint id PK varchar name UK } BOOK { bigint id PK varchar title bigint publisher_id FK } AUTHOR { bigint id PK varchar name bigint mentor_id FK } AUTHORSHIP { bigint id PK bigint book_id FK bigint author_id FK smallint position varchar role } REVIEW { bigint id PK bigint book_id FK bigint user_id FK smallint rating } CATEGORY { bigint id PK varchar name bigint parent_id FK } PROFILE { bigint id PK bigint user_id FK text bio }
Figure 1. Entity-relationship diagram of the Bookshelf models

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).

bookshelf/models.py
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.

Forward access, reverse manager and relation-spanning lookups
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

on_delete

Required. What happens to this row when the referenced row is deleted. See every on_delete option.

related_name

Name of the reverse accessor on the target. Default <model>_set (book_set). ""` (or a name ending in `) disables the reverse relation. Mandatory on abstract models, where "%(class)s" and "%(app_label)s" are interpolated.

related_query_name

Name of the reverse filter (Publisher.objects.filter(<name>__…​)). Defaults to related_name if set, else default_related_name, else the model name.

to_field

Column of the target to reference instead of the primary key. It must be unique=True.

limit_choices_to

Restricts the choices shown in a ModelForm or the admin: a dict, a Q object or a callable returning either. It does not constrain the database and is not applied on direct save(). A callable is re-evaluated each time a form is built and possibly several times by the admin.

db_constraint

True by default. False creates the column without the database foreign key.

db_index, db_column, db_default

The common field options: index (on by default for relations), column name, and, with DB_SET_DEFAULT, the required database default.

swappable

Controls how migrations record a reference to a swappable model such as the user model: by settings.AUTH_USER_MODEL (default) or by direct model. Leave it alone unless you are writing a model designed for one specific swapped-in model.

null, blank, editable, validators, unique

Field options. null=True is required for SET_NULL/DB_SET_NULL and for remove()/clear() on the reverse manager; unique=True turns the key into a one-to-one in all but name (use OneToOneField instead).

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

CASCADE

Django

Deletes the referring rows too, emulating ON DELETE CASCADE. Model.delete() is not called on them, but pre_delete and post_delete are sent for every deleted object.

PROTECT

Django

Refuses the deletion by raising ProtectedError (a subclass of django.db.IntegrityError).

RESTRICT

Django

Raises RestrictedError (also an IntegrityError), unless the referenced object is also being deleted in the same operation through a CASCADE path. Added in Django 3.1.

SET_NULL

Django

Sets the key to NULL. Requires null=True (check fields.E320 otherwise).

SET_DEFAULT

Django

Sets the key to the field’s Python default, which must be set.

SET(value_or_callable)

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.

DO_NOTHING

Nobody

No action. On a database that enforces integrity this causes an IntegrityError unless you add your own ON DELETE rule to the column.

DB_CASCADE

Database

Database-level CASCADE: the database deletes the referring rows. New in 6.1.

DB_SET_NULL

Database

Database-level SET_NULL. Requires null=True. New in 6.1.

DB_SET_DEFAULT

Database

Database-level SET_DEFAULT: the column gets its db_default value. Requires db_default; not supported on MySQL and MariaDB. New in 6.1.

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.E050 flags a model whose related fields use both levels, and fields.E323 flags a field whose referenced (or auto-created intermediary) model uses the other level; see System check framework: fields.

  • DB_SET_NULL on a non-null field is fields.E320; DB_SET_DEFAULT without db_default is fields.E322.

  • DB_SET_DEFAULT is not supported on MySQL and MariaDB (fields.E324 reports 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-level CASCADE.

  • A database-level rule applies to every client of the database: a script or a DELETE typed in psql follows it as well, which is a benefit (consistency) and a risk (the rule cannot be bypassed from Django).

A simple decision guide for the Bookshelf-style choices:

Situation Choose

Child cannot exist without the parent, few children, receivers need signals

CASCADE

Child cannot exist without the parent, very many children, no post_delete receivers

DB_CASCADE (every related model DB-level)

Parent must not be deleted while children exist

PROTECT (or RESTRICT when a cascade path should still win)

Child should outlive the parent and keep a hole

SET_NULL / DB_SET_NULL with null=True

Child should fall back to a known row

SET() with a callable, or SET_DEFAULT / DB_SET_DEFAULT

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.

ManyToManyField options

Option What it does

related_name, related_query_name, limit_choices_to

Same as on ForeignKey.

symmetrical

Only for ManyToManyField("self"). By default the relation is symmetrical (if A is B’s friend, B is A’s friend) and no reverse accessor is created; symmetrical=False makes it directional and adds one.

through

The model (class or lazy string) used as the intermediary table. See below.

through_fields

A 2-tuple (source_fk, target_fk) naming which foreign keys of the through model to use. Mandatory when the through model has more than one foreign key to either side (otherwise fields.E335).

db_table

Name of the generated join table.

db_constraint

True by default; False skips the foreign-key constraints of the join table. It is an error to pass both db_constraint and through.

swappable

As on ForeignKey.

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.

Working with a through model
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

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.

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

add(*objs, bulk=True, through_defaults=None) / aadd()

Associates existing objects. On a ForeignKey reverse it runs one QuerySet.update() (so the objects must already be saved); bulk=False calls save() on each instead. On a many-to-many it uses bulk_create() and accepts instances or primary keys; through_defaults (callables allowed, evaluated once) fills the through row. Adding an existing relation does not duplicate it.

create(through_defaults=None, **kwargs) / acreate()

Creates, saves and associates a new object in one step; you do not pass the relating field.

remove(*objs, bulk=True) / aremove()

Disassociates. On a ForeignKey it exists only if null=True; on a many-to-many it deletes through rows with QuerySet.delete() (no save() is called).

clear(bulk=True) / aclear()

Disassociates everything; same null=True rule. The related objects are not deleted.

set(objs, bulk=True, clear=False, through_defaults=None) / aset()

Replaces the set: removes what is missing and adds what is new (or, with clear=True, clears and re-adds everything). It is a compound operation and therefore subject to races.

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

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 raises ValueError ('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; one filter() call with both conditions matches the same row.

  • Cascades are bigger than they look. Always read the delete() result, and review the whole graph of on_delete rules before adding a relationship; one CASCADE can remove far more than intended, and a cascade does not call Model.delete() on the related rows (only the signals are sent).

  • related_name is 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 UniqueConstraint on the pair yourself.

  • ForeignKey cannot point at a model with a composite primary key. Django 5.2 introduced CompositePrimaryKey and relational fields do not support it yet; the documented workaround is ForeignObject with explicit from_fields and to_fields, which creates no column, constraint or index and ignores on_delete. See Constraints, Indexes & Composite Keys.

  • Migrations and relationships. Adding a non-null ForeignKey to a table with rows needs a default or a three-step migration (nullable, backfill, then non-null); switching between Python-level on_delete options produces a migration whose SQL is a no-op (the scratch project showed -- (no-op) in sqlmigrate), because the rule lives in Django, while moving to or from a DB_* option changes the constraint in the database. See Migrations.

  • Swappable user model. Reference it by settings.AUTH_USER_MODEL in relationships and with get_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

on_delete became a required argument of ForeignKey and OneToOneField in models and migrations. Examples that omit it do not run.

2.2

add(), create(), set(), get_or_create() and update_or_create() work on many-to-many relations with a custom through model, given through_defaults. Advice that these are forbidden is outdated.

3.1

RESTRICT added.

4.1

Async QuerySet interface (aget, async for, …​); a reverse manager on an unsaved instance raises ValueError.

4.2

Related managers gained aadd, aclear, aremove and aset; Model.adelete(), asave() and arefresh_from_db().

5.2 LTS

CompositePrimaryKey; relation fields cannot reference such models.

6.1

DB_CASCADE, DB_SET_NULL, DB_SET_DEFAULT; fetch modes (FETCH_ONE, FETCH_PEERS, FETCH_RAISE) and QuerySet.fetch_mode().

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).