Model Relationships

Django Fundamentals — Model Relationships
Django Fundamentals
Course 1 · Chapter 6 · Model Relationships

🔗 Model Relationships

Real data rarely lives in one table — a book has an author, a book has many tags. Django expresses those relationships as fields on a model, but the convenience of writing book.author.name hides a real performance trap this chapter's second half is dedicated to fixing: the N+1 query problem, seen here from the ORM's side rather than raw SQL.

ForeignKey: One-to-Many

An author writes many books; a book has exactly one author — that's a ForeignKey on the "many" side:

# catalog/models.py from django.db import models class Author(models.Model): name = models.CharField(max_length=150) def __str__(self): return self.name class Book(models.Model): title = models.CharField(max_length=200) author = models.ForeignKey( Author, on_delete=models.CASCADE, related_name="books", )

on_delete

Required for every ForeignKeyCASCADE deletes dependent books when their author is deleted; PROTECT blocks the delete instead; SET_NULL requires null=True and clears the reference.

related_name

Names the reverse accessor — without it, Django defaults to book_set; with related_name="books" as above, it becomes the more readable author.books.

# Forward access: book -> author (always singular, always present unless nullable) book.author.name # Reverse access: author -> books (a QuerySet, since an author can have many) author.books.all()

🕸️ ManyToManyField: Many-to-Many

A book can have several tags, and a tag applies to several books — Django manages the hidden join table for you automatically:

class Tag(models.Model): name = models.CharField(max_length=50, unique=True) def __str__(self): return self.name class Book(models.Model): title = models.CharField(max_length=200) author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books") tags = models.ManyToManyField(Tag, related_name="books", blank=True) # Usage book.tags.add(sci_fi_tag) book.tags.all() # every tag on this book sci_fi_tag.books.all() # every book with this tag, via related_name

For a many-to-many that needs extra data on the relationship itself (e.g. "when was this tag added"), Django supports an explicit through= model instead of the automatically-managed hidden table.

Querying Across Relationships

Double-underscore lookups span relationships the same way they reach into a single model's fields:

# Books written by an author named "Le Guin" Book.objects.filter(author__name="Ursula K. Le Guin") # Books tagged "sci-fi" Book.objects.filter(tags__name="sci-fi") # Authors who have written more than one book with a price under 15 Author.objects.filter(books__price__lt=15).distinct()

⚠️ The N+1 Problem, From the ORM Side

The relationship access that reads so naturally — book.author.name — hides a query every time it runs. Looping over books and touching .author on each one is the same N+1 trap the Database Integration and Rails courses warned about:

N+1 Loop vs Fixed Version

N+1: One Query Per Book
books = Book.objects.all() # 1 query for book in books: print(book.author.name) # 1 MORE query, per book (N queries) # Total: 1 + N queries for N books
Fixed: One Query, Total
books = Book.objects.select_related("author") # 1 query, SQL JOIN for book in books: print(book.author.name) # no extra query — already loaded # Total: 1 query, period

select_related()

For ForeignKey/OneToOne only — issues a single SQL JOIN, since each book has exactly one author, so the rows combine cleanly.

prefetch_related()

For ManyToMany and reverse ForeignKey — runs a second separate query and joins the results together in Python, since a JOIN would multiply rows (one book with 3 tags would otherwise return 3 duplicated book rows).

Fixing Both Relationship Types at Once

books = Book.objects.select_related("author").prefetch_related("tags") for book in books: print(book.title, book.author.name, [t.name for t in book.tags.all()]) # Exactly 2 queries total, regardless of how many books there are: # one JOINed query for books+authors, one separate query for all tags.

💻 Coding Challenges

Challenge 1: Add a ForeignKey Relationship

Add a Publisher model with a name field, then add a publisher ForeignKey on Book with on_delete=models.PROTECT and a sensible related_name. Explain in one sentence why PROTECT might be the right choice here instead of CASCADE.

Goal: Practice choosing an on_delete behavior deliberately, not just defaulting to CASCADE.

→ Solution

Challenge 2: Query Across a Many-to-Many

Using the Book/Tag models from this chapter, write a QuerySet expression for "all books tagged either 'sci-fi' or 'fantasy'," making sure the result doesn't contain duplicate books.

Goal: Practice relationship-spanning lookups combined with .distinct() where a many-to-many join could otherwise produce repeated rows.

→ Solution

Challenge 3: Fix an N+1 Query

Given a view that loops over Book.objects.all() and, for each book, prints its author's name AND every tag's name, rewrite the query to eliminate the N+1 problem for both relationships in a single fixed QuerySet.

Goal: Practice choosing select_related vs prefetch_related correctly for two different relationship types in the same query.

→ Solution

⚠️ Gotcha: Using select_related Where You Needed prefetch_related

select_related() only works for relationships where the "many" side isn't involved from this model's perspective — ForeignKey and OneToOne, where a SQL JOIN produces exactly one combined row per original row. Try it on a ManyToManyField or a reverse ForeignKey (author.books) and Django raises a FieldError — because a JOIN there would multiply rows (one author with 5 books becomes 5 duplicated author rows), which is exactly why those relationships need prefetch_related()'s separate-query-then-join-in-Python approach instead.

🎯 What's Next

Relationships between models are now readable, queryable, and efficient — the next chapter turns to getting new data in: Forms & Validation, Django's Forms and ModelForms, and its built-in CSRF protection compared to the manual setups from FastAPI and Express.