Django Models & ORM
1 · The lesson
readMany developers like the Django ORM, and many also find it frustrating at times. It is mature, short to write, and well connected to the rest of Django: Article.objects.filter(status="published").select_related("author")[:10] reads almost like English. But it also hides a lot of SQL. If you stop thinking about which queries it generates, you can easily ship an N+1 problem to production.
This lesson covers fields, the migration cycle, QuerySets, F and Q expressions, aggregations, and the eager-loading patterns (select_related / prefetch_related) that keep Django code fast as the data grows.
Run locally with
pip install djangoandpython manage.py runserver. Usepython manage.py shellfor the interactive ORM examples below.
1. A Model Is a Python Class That Maps to a Table
# blog/models.py from django.db import models from django.contrib.auth.models import User class Article(models.Model): title = models.CharField(max_length=200) slug = models.SlugField(unique=True) body = models.TextField() status = models.CharField( max_length=10, choices=[("draft", "Draft"), ("published", "Published")], default="draft", ) views = models.IntegerField(default=0) author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="articles") published_at = models.DateTimeField(null=True, blank=True) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ["-created_at"] indexes = [models.Index(fields=["status", "-created_at"])] def __str__(self): return self.title
What's happening here:
- The class inherits from
models.Modeland each attribute is a field. Django uses the fields to generate aCREATE TABLE. - An
idcolumn is added automatically as the primary key. You almost never need to declare it. ForeignKey(User, on_delete=models.CASCADE)is a real database FK.on_deleteis required. PickCASCADE(delete the article when the author is deleted),PROTECT(refuse to delete the author if articles exist),SET_NULL(keep the articles, with no author), orSET_DEFAULT.related_name="articles"gives you the reverse accessor:user.articles.all()fetches all of a user's articles. Without it, you'd writeuser.article_set.all().Metaholds the per-model config:ordering,indexes,db_table,verbose_name,unique_together,constraints. TheMeta.orderingyou set here applies to every query unless overridden.__str__is what shows in the admin and the shell. Define it on every model, because<Article: Article object (42)>tells you almost nothing.
2. The Field Tour
The fields you'll use most of the time:
| Field | SQL type | When |
|---|---|---|
CharField(max_length=N) | VARCHAR(N) | Short strings: titles, names, slugs |
TextField() | TEXT | Text of any length: bodies, descriptions |
IntegerField() | INTEGER | Counters, IDs, ages |
BigIntegerField() | BIGINT | When INTEGER might overflow (~2B) |
FloatField() / DecimalField(max_digits, decimal_places) | FLOAT / NUMERIC | Floats for science, Decimal for money |
BooleanField() | BOOLEAN | Flags. Use default=False, not null=True |
DateField() / DateTimeField() | DATE / TIMESTAMP | auto_now_add=True sets on create, auto_now=True updates on every save |
EmailField() / URLField() / SlugField() | VARCHAR with validators | Validated CharField variants |
UUIDField(default=uuid.uuid4) | UUID | When you want non-sequential primary keys |
JSONField() | JSONB (Postgres) / TEXT | Semi-structured data: config, properties |
ForeignKey(M, on_delete=...) | INTEGER REFERENCES ... | Many-to-one relationship |
OneToOneField(M, on_delete=...) | FK + UNIQUE | Profile-extends-User patterns |
ManyToManyField(M) | Through table | Tags on articles, members in groups |
Two flags every field accepts:
null=True: the column acceptsNULLin the database.blank=True: Django forms and the admin allow the field to be empty.
The two are independent. For CharField, the Django convention is to leave null=False (an empty string means "no value") and use blank to control whether forms accept an empty value. For other fields, null=True and blank=True are usually set together.
3. The Migration Cycle
Django models don't change the database directly. You describe the schema in Python, then migrations translate changes into SQL.
# After editing models.py: python manage.py makemigrations blog # generates blog/migrations/0002_*.py python manage.py migrate # applies pending migrations
makemigrations compares your current models.py with your existing migrations and writes a Python file that describes the change. Commit migrations to git, because they are part of your project's source code.
A few things to remember:
- Always review the generated migration before applying it. Look at
blog/migrations/0002_*.py: does the change look right? If you add a non-null column with no default, Django stops with "you are trying to add a non-nullable field". It wants you to decide what existing rows should contain. - Never edit a migration after it's been applied to any environment that matters. Create a new migration that makes the fix. The migration history is designed so that you only add to it.
- Squashing: once you have 47 migrations in a single app,
python manage.py squashmigrations blog 0001 0040collapses them into one. This keeps the migrations folder tidy after a year of many changes. - Data migrations: for moving data, not changing the schema. Use
RunPython(forwards_func, backwards_func)inside a migration file. Make them idempotent (safe to run more than once).
python manage.py showmigrations # what's applied, what's pending python manage.py sqlmigrate blog 0002 # show the SQL a migration would run
sqlmigrate is very useful for review: it shows exactly what SQL Django will send to the database.
4. QuerySets: Lazy, Chainable, Cached
Article.objects is a Manager. Calling things on it returns a QuerySet:
Article.objects.all() # SELECT * FROM articles Article.objects.filter(status="published") # WHERE status = 'published' Article.objects.exclude(status="draft") # WHERE NOT (status = 'draft') Article.objects.filter(views__gt=100) # WHERE views > 100 Article.objects.filter(title__icontains="django") # WHERE title ILIKE '%django%' Article.objects.filter(author__email="a@b.com") # JOIN to user, filter on email Article.objects.get(id=1) # exactly one row, or it raises DoesNotExist / MultipleObjectsReturned Article.objects.first() # first row or None Article.objects.order_by("-created_at")[:10] # ORDER BY ... LIMIT 10
setup added so this can run · defines Article
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) Article = _AutoMock('Article')
The double-underscore __ is the lookup operator: field__gt, field__lt, field__icontains, field__in, field__isnull, field__date, etc. Chain them across relationships: filter(author__profile__country="UK").
Three properties of QuerySets that often confuse people:
1. They're lazy. Building one doesn't query the database. Evaluation happens on iteration, list(), bool(), len(), slicing with a step (qs[::2]), or repr() (which is why the shell runs queries when it prints a QuerySet):
qs = Article.objects.filter(status="published") # no SQL yet qs = qs.order_by("-created_at") # still no SQL for article in qs: # NOW the query runs print(article.title)
setup added so this can run · defines Article
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) Article = _AutoMock('Article')
2. They cache. Iterating a QuerySet a second time uses the cached results, so there is no second query. But slicing a not-yet-cached queryset creates a new one:
qs = Article.objects.all() list(qs) # query runs, results cached list(qs) # cache hit, no query qs[:5] # served from the cache (qs is already evaluated) Article.objects.all()[:5] # an unevaluated queryset: slicing makes a new, lazy one
setup added so this can run · defines Article
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) Article = _AutoMock('Article')
3. Chaining returns a new QuerySet. Each call (filter, exclude, order_by) returns a new QuerySet and does not change the original. Build a base query and create new queries from it:
published = Article.objects.filter(status="published") recent = published.order_by("-created_at")[:10] featured = published.filter(featured=True)
setup added so this can run · defines Article
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) Article = _AutoMock('Article')
This gives three queries from one base, and the base does not change.
5. F Expressions: Atomic Updates
Read-modify-write in Python is a race condition. Two requests both see views=100, both compute 100 + 1, and both save 101. One view is not counted.
# Wrong: two requests at once can overwrite each other (a race condition) article = Article.objects.get(id=1) article.views += 1 article.save()
setup added so this can run · defines Article
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) Article = _AutoMock('Article')
F expressions move the arithmetic into SQL, so the database does it in one atomic step:
from django.db.models import F Article.objects.filter(id=1).update(views=F("views") + 1)
setup added so this can run · defines Article
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) Article = _AutoMock('Article')
The generated SQL is UPDATE articles SET views = views + 1 WHERE id = 1: one atomic statement, with no race condition. Use F expressions any time the new value depends on the current value.
F also lets you compare two columns:
Article.objects.filter(updated_at__gt=F("created_at")) # edited articles Author.objects.filter(books_written__lt=F("books_promised"))
setup added so this can run · defines Article, F, Author
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) Article = _AutoMock('Article') def F(*_a, **_kw): print('-> F() called') return _AutoMock('F()') Author = _AutoMock('Author')
6. Q Objects: OR and Complex Boolean Logic
filter(a=1, b=2) is AND. For OR, you need Q objects:
from django.db.models import Q # WHERE status = 'published' OR featured = True Article.objects.filter(Q(status="published") | Q(featured=True)) # WHERE (status='published' OR featured=True) AND views > 100 Article.objects.filter(Q(status="published") | Q(featured=True), views__gt=100) # NOT Article.objects.filter(~Q(status="draft"))
setup added so this can run · defines Article
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) Article = _AutoMock('Article')
Operators: | for OR, & for AND, ~ for NOT. Use Q for any OR condition, because chaining .filter() gives AND, and there is no .or_filter.
7. Aggregations and Annotations
Two ways to compute values with SQL:
aggregate: turn a QuerySet into a single dict of numbers.annotate: add a computed column to each row in the QuerySet.
from django.db.models import Count, Avg, Max, Sum # aggregate: one row of stats Article.objects.aggregate( total=Count("id"), avg_views=Avg("views"), most_viewed=Max("views"), ) # {'total': 142, 'avg_views': 387.5, 'most_viewed': 12450} # annotate: one extra column per row authors = User.objects.annotate( article_count=Count("articles"), total_views=Sum("articles__views"), ).order_by("-article_count") for author in authors: print(author.username, author.article_count, author.total_views)
setup added so this can run · defines Article, User
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) Article = _AutoMock('Article') User = _AutoMock('User')
annotate is the tool you use most for leaderboards, dashboards and any "show me each X with its count of Y". The generated SQL uses GROUP BY and joins, which is much faster than a Python loop with N queries.
8. The N+1 Problem and How to Fix It
This is the most typical ORM mistake. The code looks harmless, but under load it can overload the database.
# Template: 1 query for articles, then 1 per article for its author. This is N+1. articles = Article.objects.filter(status="published") for article in articles: print(article.title, article.author.username) # lazy-loads author EACH TIME
setup added so this can run · defines Article
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) Article = _AutoMock('Article')
50 articles cause 51 queries. With production traffic, this can make the site go down.
Django offers two fixes, depending on the relationship:
select_related: for ForeignKey and OneToOneField (the "many-to-one" side). It runs a SQL JOIN and returns one row per article with author columns included.
articles = Article.objects.select_related("author").filter(status="published") for article in articles: print(article.title, article.author.username) # no extra query
setup added so this can run · defines Article
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) Article = _AutoMock('Article')
prefetch_related: for ManyToManyField and reverse ForeignKey (the "one-to-many" side). It runs a second query with WHERE id IN (...) and joins the results in Python.
articles = Article.objects.prefetch_related("tags").all() for article in articles: print(article.title, [t.name for t in article.tags.all()]) # tags pre-loaded
setup added so this can run · defines Article
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) Article = _AutoMock('Article')
You can chain across relationships with double-underscore: select_related("author__profile"), prefetch_related("comments__author").
To find N+1 problems, turn on DEBUG=True and django-debug-toolbar, or set LOGGING to log every SQL query, or use connection.queries in the shell. Any time you see one query per row in a loop, that's an N+1.
9. Transactions
Django runs in autocommit mode by default. Set ATOMIC_REQUESTS=True in DATABASES to wrap each HTTP request in a transaction. For finer control:
from django.db import transaction @transaction.atomic def transfer(from_id, to_id, amount): Account.objects.filter(id=from_id).update(balance=F("balance") - amount) Account.objects.filter(id=to_id).update(balance=F("balance") + amount)
setup added so this can run · defines F, Account
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) def F(*_a, **_kw): print('-> F() called') return _AutoMock('F()') Account = _AutoMock('Account')
If the function raises, the whole transaction rolls back. As a context manager:
with transaction.atomic(): Article.objects.filter(status="draft").update(status="archived") Author.objects.filter(active=False).delete()
setup added so this can run · defines transaction, Article, Author
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) transaction = _AutoMock('transaction') Article = _AutoMock('Article') Author = _AutoMock('Author')
For long-running tasks (a 30-minute report), don't keep a transaction open, because it can lock rows that other requests need.
10. Custom Managers
The default manager is objects. You can replace or extend it:
class PublishedManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(status="published") class Article(models.Model): # ... fields ... objects = models.Manager() # the default manager, which returns all rows published = PublishedManager() # only published rows
setup added so this can run · defines models
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) models = _AutoMock('models')
Now Article.published.all() returns only published articles, and you can add more filters with Article.published.filter(featured=True). It is the cleanest way to avoid repeating "default scope" logic in every view.
A similar approach is a QuerySet subclass plus Manager.from_queryset(...). The idea is the same, but the methods you add can be chained. The Django docs explain it well. Use it when you find yourself writing filter(status="published") in fifteen places.
11. The Admin, More Carefully
We saw admin.site.register(Article) in the previous lesson. Real admin classes usually customise a lot:
@admin.register(Article) class ArticleAdmin(admin.ModelAdmin): list_display = ("title", "author", "status", "views", "created_at") list_filter = ("status", "author") search_fields = ("title", "body") date_hierarchy = "created_at" raw_id_fields = ("author",) # show the FK as an ID box with a lookup, not a 10,000-option dropdown list_select_related = ("author",) # avoids N+1 in the list view itself readonly_fields = ("created_at", "updated_at") prepopulated_fields = {"slug": ("title",)}
setup added so this can run · defines admin, Article
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) admin = _AutoMock('admin') Article = _AutoMock('Article')
The list_select_related line is the admin's own N+1 fix. Without it, the list page runs one query per row to get the author. With it, there is one JOIN.
12. Signals: Useful, but Easy to Misuse
Django emits signals at key model events: pre_save, post_save, pre_delete, post_delete, m2m_changed.
from django.db.models.signals import post_save from django.dispatch import receiver @receiver(post_save, sender=Article) def create_audit_log(sender, instance, created, **kwargs): if created: AuditLog.objects.create(action="article-created", target=instance.id)
setup added so this can run · defines Article, AuditLog
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) Article = _AutoMock('Article') AuditLog = _AutoMock('AuditLog')
Useful for cross-cutting concerns (audit logs, cache invalidation, search indexing). The problem: signals hide what happens. The code that saves an Article doesn't show that saving it also creates a log entry, sends an email and re-indexes Elasticsearch. It becomes hard to find out why a side effect happened.
Rule of thumb: if the action is logically part of saving the article, override save(). Use signals only when the receiver lives in a different app and can't be coupled to the sender.
13. Common Mistakes
1. N+1 in templates. {% for article in articles %}{{ article.author.username }}{% endfor %} looks harmless, but it runs one query per article. Add .select_related("author") to the view's queryset.
2. Calling list methods on QuerySets. A QuerySet is not a list. qs.append(x) doesn't exist. You create instances with Model.objects.create(...). People also confuse it with qs[0] (which works) and qs.add(x) (which exists only on related managers, such as m2m managers).
3. Editing migrations after applying them. Once 0042_add_field.py has been applied to staging, changing it means your local migration no longer matches what staging's django_migrations table says was applied. Create 0043_fix_field.py instead.
4. Forgetting on_delete on ForeignKey. Django requires you to pick. The right answer depends on what the data means: CASCADE for "child rows can't exist without the parent", PROTECT to refuse deletion, SET_NULL for "keep the child rows without a parent". There is no good default, so think about each case.
5. Using .get() when you want .first(). Model.objects.get(...) raises DoesNotExist for zero rows and MultipleObjectsReturned for >1. Wrap it in try/except or use .filter(...).first() if "not found" is normal.
6. values() vs values_list() vs model instances. Article.objects.values("title", "views") returns dicts. .values_list("title", "views") returns tuples (add flat=True for a flat list of one column). Use these when you need raw data and don't need model objects. They are much faster than creating a model instance for every row.
7. Putting derived fields on the model when they could be properties. A total_price that's always quantity * unit_price doesn't need a column. Use a @property instead. If you store derived data, you must keep it in sync forever.
🎯 Your Turn: Tags With No N+1
Extend the tasks app from the previous lesson. Add a Tag model and a many-to-many relationship from Task. Then write a view that lists tasks with their tags, using prefetch_related to avoid N+1.
Requirements:
1. Define a Tag model with a unique name (max 50 chars).
2. Add tags = ManyToManyField(Tag, blank=True, related_name="tasks") to Task.
3. Run makemigrations tasks and migrate.
4. Write a task_list_with_tags view that returns all tasks, eager-loading tags.
5. Render each task with its tag names.
6. Verify in the shell that the query count is constant (not proportional to the number of tasks).
Skeleton:
# tasks/models.py from django.db import models class Tag(models.Model): name = models.CharField(max_length=50, unique=True) def __str__(self): return self.name class Task(models.Model): title = models.CharField(max_length=200) done = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) # TODO 1: add a tags M2M field class Meta: ordering = ["-created_at"] def __str__(self): return self.title
# tasks/views.py from django.shortcuts import render from .models import Task def task_list_with_tags(request): # TODO 2: build a queryset that eager-loads tags # TODO 3: render to "tasks/task_list_tagged.html" ...
{# tasks/templates/tasks/task_list_tagged.html #}
<h1>Tasks</h1>
<ul>
{# TODO 4: for each task, show its title plus a comma-separated list of tag names #}
</ul>To verify the query count:
python manage.py shell >>> from django.db import connection, reset_queries >>> from django.conf import settings >>> settings.DEBUG = True >>> reset_queries() >>> from tasks.models import Task >>> # TODO 5: build the queryset, force evaluation, then count connection.queries
Hint 1: prefetch_related vs select_related
Tags areManyToManyField, so it's prefetch_related("tags"), not select_related. select_related is for ForeignKey / OneToOneField, the "many-to-one" side where a JOIN works.
Hint 2: Looping over the M2M in the template
{% for tag in task.tags.all %}{{ tag.name }}{% if not forloop.last %}, {% endif %}{% endfor %}. Without the prefetch, this runs one query per task. With the prefetch, the data is already in memory and Django reads it from there.
Show full solution
# tasks/models.py from django.db import models class Tag(models.Model): name = models.CharField(max_length=50, unique=True) def __str__(self): return self.name class Task(models.Model): title = models.CharField(max_length=200) done = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) tags = models.ManyToManyField(Tag, blank=True, related_name="tasks") class Meta: ordering = ["-created_at"] def __str__(self): return self.title
# tasks/views.py from django.shortcuts import render from .models import Task def task_list_with_tags(request): tasks = Task.objects.prefetch_related("tags").all() return render(request, "tasks/task_list_tagged.html", {"tasks": tasks})
# tasks/urls.py from django.urls import path from . import views urlpatterns = [ path("", views.task_list, name="task-list"), path("tagged/", views.task_list_with_tags, name="task-list-tagged"), ]
{# tasks/templates/tasks/task_list_tagged.html #}
<!doctype html>
<html>
<head><meta charset="utf-8"><title>Tasks with tags</title></head>
<body>
<h1>Tasks</h1>
<ul>
{% for task in tasks %}
<li>
{% if task.done %}<s>{{ task.title }}</s>{% else %}{{ task.title }}{% endif %}
{% if task.tags.all %}
Tags:
{% for tag in task.tags.all %}{{ tag.name }}{% if not forloop.last %}, {% endif %}{% endfor %}
{% endif %}
</li>
{% empty %}
<li>No tasks yet.</li>
{% endfor %}
</ul>
</body>
</html>Apply the migration:
python manage.py makemigrations tasks python manage.py migrate
Verify in the shell:
$ python manage.py shell >>> from django.db import connection, reset_queries >>> from django.conf import settings >>> settings.DEBUG = True >>> from tasks.models import Task, Tag >>> >>> # Seed some data >>> t1, t2, t3 = (Task.objects.create(title=f"Task {i}") for i in range(3)) >>> work, urgent = Tag.objects.create(name="work"), Tag.objects.create(name="urgent") >>> t1.tags.add(work, urgent); t2.tags.add(work); t3.tags.add(urgent) >>> >>> # Without prefetch: 1 query for tasks + 1 per task for tags = N+1 >>> reset_queries() >>> tasks = list(Task.objects.all()) >>> _ = [list(t.tags.all()) for t in tasks] >>> len(connection.queries) 4 # 1 for tasks, 3 for tag lookups >>> >>> # With prefetch: 1 query for tasks + 1 for all tags = 2 total, regardless of N >>> reset_queries() >>> tasks = list(Task.objects.prefetch_related("tags").all()) >>> _ = [list(t.tags.all()) for t in tasks] >>> len(connection.queries) 2 # constant
That is two queries instead of N+1. On a page of 100 tasks, you save 99 round-trips to the database.
What You Learned
- Models map to tables. Define fields as class attributes, set
Meta.ordering, define__str__. TheidPK is automatic. - Migrations are an append-only Python record of schema changes.
makemigrations→ review →migrate. Commit them. Never edit applied ones. - QuerySets are lazy, chainable, cached. Building one doesn't query, but iterating does. Each chain call returns a new QuerySet.
- F expressions for atomic updates that depend on current values, and Q objects for OR and complex boolean filters.
aggregatecollapses, andannotateadds a value to each row.Count,Sum,AvgandMaxare useful for dashboards.- N+1 is the most typical ORM mistake.
select_relatedfor FK (one JOIN), andprefetch_relatedfor M2M (second query, joined in Python). - Transactions with
@transaction.atomicorwith transaction.atomic():. Don't hold them open across slow work. - Custom managers keep "default scope" logic out of every view.
- Signals are useful but easy to misuse. Prefer overriding
save()when the logic belongs to the model.
Next: Django REST Framework: turning these models into a real JSON API with serializers, viewsets, and permissions.
Found this useful? Share it with a friend who’s learning Python.