Filter concepts by levelShowing all levels.

Django · Section 14

Relationships

Level
intermediate
Read
22 min
Concepts
3

The three relationship fields (ForeignKey, OneToOneField, ManyToManyField) and their naming options, the automatic reverse manager every relationship gets plus through models for a many-to-many relationship that needs its own extra fields, and self-referencing relationships — trees via a self-referencing ForeignKey, and mutual-vs-one-directional links via a self-referencing ManyToManyField's symmetrical argument.

This section

What is true here

  1. ForeignKey is many-to-one; OneToOneField is the same field plus a uniqueness constraint; ManyToManyField is a separate join table — none of the three default on_delete, it must always be set explicitly.
  2. Every relationship field creates a reverse manager automatically (<model>_set by default) — related_name renames it, and related_query_name independently renames the filter() keyword.
  3. A through model replaces ManyToManyField's invisible auto-generated join table with a real model, needed the moment the relationship itself needs extra fields.
  4. "self" (the literal string) references the model currently being defined, since the class body executes before the class name itself exists.
  5. A self-referencing ManyToManyField defaults to symmetrical=True (mutual, like friends) — symmetrical=False is required for a one-directional relationship like follows.

What you will be able to do

  • Choose the right relationship field for a given cardinality, and set on_delete deliberately
  • Name reverse accessors and filter() keywords with related_name/related_query_name
  • Recognize when a plain ManyToManyField needs to become a through model
  • Model a tree or a mutual/one-directional self-referencing relationship correctly

The three relationship fields

ForeignKey, OneToOneField, and ManyToManyField — the same underlying idea, three different uniqueness rules — plus naming the reverse side.

ForeignKey, OneToOneField, and ManyToManyField

coreintermediate

ForeignKey is many-to-one (many Orders, one Customer). OneToOneField enforces uniqueness on top of the same idea — exactly one match each way (one User, one Profile). ManyToManyField lets both sides have many matches (many Students, many Courses). related_name renames the reverse accessor (customer.order_set becomes customer.orders); related_query_name renames the reverse lookup keyword used in filter().

Think of it as

Picture the three fields as the same underlying idea — a link between two tables — with a different UNIQUE constraint applied on top. ForeignKey adds none: any number of rows on the "many" side can point at the same row on the "one" side. OneToOneField is a ForeignKey with a UNIQUE constraint bolted onto the FK column — so at most one row can ever point at each target. ManyToManyField is not a column at all; it is a whole separate join table with two foreign keys, one to each side, because a single column cannot hold "many" references.

python
customer = models.ForeignKey(
    "Customer", on_delete=models.PROTECT, related_name="orders", related_query_name="order",
)

What we're doing: Give an Order's ForeignKey to Customer a readable reverse accessor and a matching query keyword.

orders/models.pypython
class Order(models.Model):
    customer = models.ForeignKey(
        "Customer", on_delete=models.PROTECT, related_name="orders", related_query_name="order",
    )
    total = models.DecimalField(max_digits=10, decimal_places=2)

# usage:
customer.orders.all()                                  # related_name
Customer.objects.filter(order__total__gt=100)           # related_query_name
2
related_name="orders" replaces the default customer.order_set with customer.orders — the plural, model-name-based default is functional but rarely the name a reader would reach for first.
3
related_query_name="order" is independent of related_name — it controls the SINGULAR keyword used inside filter(), separately from the plural accessor used for direct traversal.

Why this works: The default order_set works but reads as generated code; related_name gives every reverse relationship a name that matches how the codebase actually talks about it, and related_query_name does the same for the filter() keyword, which defaults to the lowercased model name and can diverge in style from related_name if left unset.

Reusing the same related_name on two ForeignKeys to the same target model

Wrong

python
class Order(models.Model):
    billing_customer = models.ForeignKey("Customer", on_delete=models.PROTECT, related_name="orders")
    shipping_customer = models.ForeignKey("Customer", on_delete=models.PROTECT, related_name="orders")

Better

python
class Order(models.Model):
    billing_customer = models.ForeignKey("Customer", on_delete=models.PROTECT, related_name="billed_orders")
    shipping_customer = models.ForeignKey("Customer", on_delete=models.PROTECT, related_name="shipped_orders")

What you see: django.core.exceptions.FieldError: Reverse accessor for 'Order.shipping_customer' clashes with reverse accessor for 'Order.billing_customer' — raised at startup (system check), before any request is even handled.

Why: Both ForeignKeys point at the same target model (Customer), so both would try to create the exact same reverse accessor name on Customer unless each gets its own distinct related_name — one column pointing at Customer is fine with the default name, but two or more from the same model require explicit, distinct names.

Cardinality vs. underlying structure
ForeignKey
many-to-one, a column on the "many" side
OneToOneField
ForeignKey + unique=True
ManyToManyField
a real join table, two foreign keys
  • ForeignKey: plain column, unconstrained — many-to-one, a column on the "many" side
  • OneToOneField: plain column, unique — ForeignKey + unique=True
  • ManyToManyField: separate join table, between unconstrained and unique — a real join table, two foreign keys

The three relationship fields

The three relationship fields
FieldCardinalityUnderlying structure
ForeignKeymany-to-onea column on the "many" side, storing the related row's id
OneToOneFieldone-to-onethe same column, plus a UNIQUE constraint
ManyToManyFieldmany-to-manya separate join table with two foreign keys

Together

python
class Order(models.Model):
    customer = models.ForeignKey("Customer", on_delete=models.PROTECT, related_name="orders")

class Profile(models.Model):
    user = models.OneToOneField("auth.User", on_delete=models.CASCADE, related_name="profile")

class Course(models.Model):
    students = models.ManyToManyField("Student", related_name="courses")

Remember: ForeignKey (many-to-one), OneToOneField (ForeignKey + unique=True), ManyToManyField (a separate join table, no on_delete needed) — related_name renames the reverse accessor, related_query_name renames the filter() keyword, independently.

See also: reverse relations and through models · the seven on delete options · primary keys and field options

Advertisement

Reverse relations and through models

The automatic reverse manager every relationship field creates, and when a many-to-many relationship needs a real model of its own.

Reverse relations and through models

coreintermediate

Every ForeignKey/ManyToManyField automatically creates a reverse manager on the OTHER model — Model.objects.filter(...) plus the default <model>_set (or related_name, if set) to go the other way. A through model replaces ManyToManyField's auto-generated join table with a real model of your own, needed the moment the relationship itself needs extra fields (grade, joined_at) beyond just "these two are linked."

Think of it as

A plain ManyToManyField's join table is invisible — Django creates and manages it, and there is no model to import or query directly. The moment the relationship itself needs data (WHEN did this student enroll, WHAT grade did they get), that invisible table has to become a visible, first-class model — a through model. Nothing about the relationship's cardinality changes; what changes is whether the link between two rows carries information of its own, not just the fact of the link.

python
Enrollment.objects.create(student=alice, course=math_101, grade="A")   # through model
customer.orders.all()                                                    # reverse relation

What we're doing: Track when a student enrolled and their grade — data about the relationship itself, not about either Student or Course alone.

school/models.pypython
class Course(models.Model):
    students = models.ManyToManyField("Student", through="Enrollment", related_name="courses")

class Enrollment(models.Model):
    student = models.ForeignKey("Student", on_delete=models.CASCADE)
    course = models.ForeignKey("Course", on_delete=models.CASCADE)
    grade = models.CharField(max_length=2, blank=True)
    enrolled_at = models.DateField(auto_now_add=True)
2
through="Enrollment" tells Django not to auto-generate the join table — Enrollment IS the join table, as a real model.
6
grade has nowhere to live on a plain ManyToManyField — it belongs to the LINK between a specific student and a specific course, not to either row alone, which is exactly what a through model is for.
7
enrolled_at is a second example of the same idea — a timestamp about the relationship, not about Student or Course individually.

Why this works: A plain students = models.ManyToManyField("Student") records only THAT a student and course are linked — the moment a grade or an enrollment date needs to be tracked, that information has nowhere to go unless the join table itself becomes a real, queryable model.

Trying to use .add()/.set() on a ManyToManyField whose through model has extra required fields

Wrong

python
course.students.add(alice)   # fails once Enrollment.grade has no default and is required

Better

python
Enrollment.objects.create(student=alice, course=course, grade="A")

What you see: django.db.utils.IntegrityError (or a Django-raised error depending on version) when calling .add()/.set()/.create() directly on the ManyToManyField, once a through model with a required extra field is set.

Why: Django cannot know what value to put in Enrollment.grade when .add() is called directly on the ManyToManyField — once the through model has fields beyond the two foreign keys, the relationship has to be created through the through model's own manager, which can supply those extra fields explicitly.

A through model makes the join table a real, queryable model

Student

id

Enrollment (through)

student, course

grade, enrolled_at

Course

id

  • Student
    • id
  • Enrollment (through)
    • student, course
    • grade, enrolled_at
  • Course
    • id

Plain ManyToManyField vs a through model

Plain ManyToManyField vs a through model
AspectPlain ManyToManyFieldthrough model
Join tableauto-created, invisiblea real model you define
Extra fields on the relationshipnot possibleyes — any fields the through model declares
Adding a link.add(obj)Enrollment.objects.create(student=..., course=..., grade=...)
Querying the relationship itselfnot directly possibleEnrollment.objects.filter(grade="A")

Together

python
class Course(models.Model):
    students = models.ManyToManyField("Student", through="Enrollment", related_name="courses")

class Enrollment(models.Model):
    student = models.ForeignKey("Student", on_delete=models.CASCADE)
    course = models.ForeignKey("Course", on_delete=models.CASCADE)
    grade = models.CharField(max_length=2, blank=True)
    enrolled_at = models.DateField(auto_now_add=True)

Remember: Every relationship field gets a reverse manager automatically (<model>_set by default, renamed via related_name) — a through model is needed only when the LINK itself needs extra fields, and once set, its own manager (not .add()/.set()) creates the relationship.

See also: the three relationship fields · self referencing relationships · many to many and reverse fk

Advertisement

Self-referencing relationships

Trees and mutual/one-directional links, both pointing a model back at itself.

Self-referencing relationships

standardintermediate

A model can have a ForeignKey or ManyToManyField pointing at ITSELF — pass the string "self" instead of the model's name, since the class isn't fully defined yet at the point the field is declared. Common shapes: a tree (Category.parent points at another Category) and a symmetrical or non-symmetrical friendship/follow graph (User.objects.filter... via a self-referencing ManyToManyField).

Think of it as

A self-referencing ForeignKey is a parent/child pointer within one table — a Category row's parent_category column stores another Category's id, exactly like a linked list or a tree, all inside a single table. A self-referencing ManyToManyField adds a choice most other relationships don't need to make: symmetrical (if A follows B, does B automatically follow A too — like Facebook friends) or non-symmetrical (A can follow B without B following A — like Twitter/X follows), controlled by ManyToManyField's symmetrical argument.

python
parent = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE, related_name="children")

What we're doing: Model a Category tree — User → Profile → Orders/Roles shaped roadmap tree, applied here to a self-referencing parent/child hierarchy.

catalog/models.pypython
class Category(models.Model):
    name = models.CharField(max_length=100)
    parent = models.ForeignKey(
        "self", null=True, blank=True, on_delete=models.CASCADE, related_name="children",
    )

# usage:
electronics = Category.objects.create(name="Electronics")
laptops = Category.objects.create(name="Laptops", parent=electronics)
electronics.children.all()   # [<Category: Laptops>] — via related_name
3
"self" (a string, not the Category class) is required here — at the point this line runs, the Category class body is still being defined, so the bare name Category does not exist yet.

Why this works: related_name="children" makes the reverse direction read naturally (electronics.children.all()) — without it, the default reverse accessor would be category.category_set, which is technically correct but reads as generated code rather than a real tree relationship.

Using the bare class name instead of "self" inside the class body

Wrong

python
class Category(models.Model):
    parent = models.ForeignKey(Category, null=True, on_delete=models.CASCADE)   # NameError

Better

python
class Category(models.Model):
    parent = models.ForeignKey("self", null=True, on_delete=models.CASCADE)

What you see: NameError: name 'Category' is not defined — raised at import time, since the class body executes top-to-bottom and Category does not exist as a name until the entire class statement finishes.

Why: Python evaluates a class body's statements before the class itself is bound to its name — "self" is Django's own string shorthand specifically for this situation, resolved lazily by Django rather than looked up as a Python name at class-definition time.

Self-referencing ForeignKey vs ManyToManyField

Self-referencing ForeignKey vs ManyToManyField
ShapeFieldSymmetrical?
Tree (category → parent category)ForeignKey("self", null=True, on_delete=...)n/a — not a M2M
Mutual link (friends)ManyToManyField("self")True (default) — adding A→B also implies B→A
One-directional link (follows)ManyToManyField("self", symmetrical=False)False — A→B does not imply B→A

Together

python
class Category(models.Model):
    name = models.CharField(max_length=100)
    parent = models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE, related_name="children")

class Profile(models.Model):
    follows = models.ManyToManyField("self", symmetrical=False, related_name="followed_by")

Remember: "self" (the string) references the model currently being defined; ManyToManyField("self") defaults to symmetrical=True (mutual) — a one-directional relationship (follows, not friends) needs symmetrical=False set explicitly.

See also: the three relationship fields · reverse relations and through models · the seven on delete options

Advertisement