Model Relationships
🔗 Model Relationships
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:
on_delete
Required for every ForeignKey — CASCADE 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.
🕸️ 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:
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:
⚠️ 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
Fixed: One Query, Total
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
💻 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.
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.
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.
select_related Where You Needed prefetch_relatedselect_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.