QuerySets and Model Relationships
Introduction
Real applications connect models: a post belongs to an author, tags attach to many posts. This chapter adds ForeignKey and ManyToManyField, explores reverse relations, filters across joins, and introduces select_related and prefetch_related to avoid N+1 query problems.
Prerequisites
- Models and ORM Basics —
Postmodel and migrations - Views — list/detail views
Extend Models with Relationships
Author (ForeignKey)
blog/models.py:
Migrate:
python manage.py makemigrations blog
python manage.py migrateCode explanation:
| Relation | DB effect |
|---|---|
ForeignKey(User) | Column author_id on blog_post |
on_delete=CASCADE | Delete user → delete their posts |
related_name="posts" | user.posts.all() instead of post_set |
ManyToManyField | Junction table blog_post_tags |
Use settings.AUTH_USER_MODEL string ("auth.User") so custom user models stay compatible.
Forward and Reverse Access
Default reverse name without related_name: post_set — explicit related_name reads cleaner.
Filtering Across Relations
Double underscore spans joins:
Post.objects.filter(author__username="alice")
Post.objects.filter(tags__slug="django")
Post.objects.filter(author__is_active=True, published=True)
User.objects.filter(posts__published=True).distinct().distinct() needed when M2M joins duplicate parent rows.
Lookups: __icontains, __gte, __in, __isnull.
Create with Relations
user = User.objects.get(username="admin")
tag, _ = Tag.objects.get_or_create(name="Django", slug="django")
post = Post.objects.create(
title="Relations demo",
slug="relations-demo",
body="Content",
author=user,
published=True,
)
post.tags.add(tag)
post.tags.set([tag]) # replace all tagsselect_related (ForeignKey)
Without optimization, listing posts may query the user table once per row:
# N+1: 1 query for posts + N for each author
posts = Post.objects.filter(published=True)
for p in posts:
print(p.author.username)Fix with select_related (SQL JOIN):
posts = Post.objects.filter(published=True).select_related("author")
for p in posts:
print(p.author.username) # no extra queryUse for ForeignKey and OneToOneField (single-valued joins).
prefetch_related (ManyToMany, reverse FK)
M2M and reverse relations use separate queries batched in Python:
posts = (
Post.objects.filter(published=True)
.select_related("author")
.prefetch_related("tags")
)
for p in posts:
print(p.author.username, list(p.tags.all()))For deep trees:
.prefetch_related("tags", "comments__author")Aggregation Preview
from django.db.models import Count, Max
Post.objects.filter(published=True).count()
Post.objects.aggregate(latest=Max("created_at"))
Tag.objects.annotate(post_count=Count("posts")).order_by("-post_count")Full aggregation patterns appear in project chapters; annotate + Count is common for tag clouds.
Update Views with Relations
blog/views.py:
def post_list(request):
posts = (
Post.objects.filter(published=True)
.select_related("author")
.prefetch_related("tags")
)
return render(request, "blog/post_list.html", {"posts": posts})Template:
{% for post in posts %}
<article>
<h2><a href="{% url 'post-detail' pk=post.id %}">{{ post.title }}</a></h2>
<p>By {{ post.author.username }}</p>
<p>
{% for tag in post.tags.all %}
<span class="tag">{{ tag.name }}</span>
{% endfor %}
</p>
</article>
{% endfor %}on_delete Options
| Value | Behavior |
|---|---|
CASCADE | Delete related rows |
PROTECT | Block delete if related exist |
SET_NULL | Set FK NULL (null=True required) |
SET_DEFAULT | Set default= value |
DO_NOTHING | DB-level only ( risky ) |
Choose PROTECT for categories you must not orphan accidentally.
Custom User Model (Awareness)
Projects often start with:
# users/models.py
from django.contrib.auth.models import AbstractUser
class User(AbstractUser):
bio = models.TextField(blank=True)Set AUTH_USER_MODEL = "users.User" before first migrate. Changing later is painful—plan early for production apps.
Debugging Queries
Development logging in settings/development.py:
LOGGING = {
"version": 1,
"handlers": {"console": {"class": "logging.StreamHandler"}},
"loggers": {"django.db.backends": {"level": "DEBUG", "handlers": ["console"]}},
}Or:
print(Post.objects.filter(published=True).query)Use django-debug-toolbar in real projects (not required for this track).
Common Mistakes
| Mistake | Symptom | Fix |
|---|---|---|
| N+1 queries | Slow list pages | select_related / prefetch_related |
Forgot .distinct() | Duplicate users in list | After filtering on M2M |
Wrong related_name | AttributeError | Check model definition |
CASCADE on critical FK | Accidental mass delete | PROTECT or soft delete |
Post-Chapter Checklist
-
Post.authorForeignKeyandPost.tagsM2M migrated - You query
user.posts.all()andpost.tags.all() - You filter with
author__username,tags__slug - List view uses
select_related("author")andprefetch_related("tags") - You understand
on_deletechoices
FAQ
related_name vs related_query_name?
related_query_name defaults to related_name; used in filter(posts__...) from User side.
Nullable author?
null=True, blank=True on FK — posts without author allowed.
Through model for M2M extra fields?
Add through="PostTag" with created_at on junction — eCommerce carts use this pattern.
GenericForeignKey?
Contenttypes framework — comments on any model — advanced.
PostgreSQL only features?
ArrayField, JSONField history — modern Django supports JSONField on MySQL too.