Django Models & ORM
1 · The lesson
readThe Django ORM is the part of the framework people love or fight, often both in the same week. It's mature, terse, and well-integrated — Article.objects.filter(status="published").select_related("author")[:10] reads almost like English. It also hides a lot of SQL, and the moment you stop thinking about what queries it actually generates, you end up shipping an N+1 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 separate Django code that scales from Django code that grinds to a halt.
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 these 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 mandatory — pickCASCADE(delete the article when the author is deleted),PROTECT(refuse to delete the author if articles exist),SET_NULL(orphan the articles), orSET_DEFAULT.related_name="articles"gives you the reverse accessor —user.articles.all()to fetch all of a user's articles. Without it, you'd writeuser.article_set.all().Metais 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 —<Article: Article object (42)>is useless.
2. The Field Tour
The fields you'll use 95% of the time:
| Field | SQL type | When |
|---|---|---|
CharField(max_length=N) | VARCHAR(N) | Short strings — titles, names, slugs |
TextField() | TEXT | Unbounded prose — 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, Django convention is to leave null=False (empty string is the absence value) and toggle blank for form acceptance. For everything else, null=True and blank=True usually move together.
3. The Migration Cycle
Django models don't touch 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 diffs your current models.py against the last applied migration and writes a Python file describing the change. Commit migrations to git — they're part of the source of truth.
A few things to internalise:
- Always review the generated migration before applying it. Look at
blog/migrations/0002_*.py— does the change look right? Adding a non-null column with no default will halt with "you are trying to add a non-nullable field" — Django is making sure you've thought about what existing rows should contain. - Never edit a migration after it's been applied to any environment that matters. Create a new migration that adjusts. The migration history is append-only by design.
- Squashing — once you have 47 migrations on a single app,
python manage.py squashmigrations blog 0001 0040collapses them into one. Useful for keeping the migrations folder tidy after a year of churn. - Data migrations — for moving data, not schema. Use
RunPython(forwards_func, backwards_func)inside a migration file. Keep them idempotent.
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 invaluable for review — see exactly what Django plans to 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 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 trip people up:
1. They're lazy. Building one doesn't hit the database. Evaluation happens on iteration, list(), bool(), len(), slicing with a stop, or repr() (the shell trap):
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 — 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] # new queryset, would query again
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 fresh QuerySet — no mutation. Build a base query and branch 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')
Three queries, one base, no surprises.
5. F Expressions — Atomic Updates
Read-modify-write in Python is a race condition. Two requests both see views=100, both compute 100 + 1, both save 101. You've lost a view.
# WRONG — race condition under concurrency 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 push the arithmetic into SQL — atomic at the database level:
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 statement, atomic, no race. 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. Wrap any non-trivial OR in Q — chaining .filter() is AND and there's no .or_filter.
7. Aggregations and Annotations
The two halves of statistical SQL:
aggregate— collapse 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 workhorse for leaderboards, dashboards, and any "show me each X with its count of Y". The generated SQL uses GROUP BY and joins — way faster than a Python loop with N queries.
8. The N+1 Problem and How to Fix It
The defining ORM footgun. Looks innocent, costs you the database under load.
# Template: 1 query for articles, then 1 per article for its author. 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 fires 51 queries. Production traffic turns this into an outage.
Django offers two fixes, depending on the relationship:
select_related — for ForeignKey and OneToOneField (the "many-to-one" side). Issues a SQL JOIN, 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). Issues 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").
The detection: 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 wraps each HTTP request in a transaction by default (with ATOMIC_REQUESTS=True in DATABASES). 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 hold a transaction open — you'll 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 — 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 layer further with Article.published.filter(featured=True). It's the cleanest way to keep "default scope" logic out of every view.
A close cousin is the QuerySet subclass plus Manager.from_queryset(...) — same idea, but methods you add are chainable. The Django docs are good here; reach for 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-world admin classes customise heavily:
@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",) # FK as ID + lookup widget, not a 10K-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, rendering the list page fires one query per row for the author. With it, one JOIN.
12. Signals — Useful, Easy to Abuse
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 trap: signals make code spooky — the call site has no idea that saving an Article also creates a log entry, sends an email, and re-indexes Elasticsearch. Debugging "why did this side-effect happen?" gets painful.
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 innocent and fires 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(...). Confusion with qs[0] (which works) and qs.add(x) (only on m2m managers).
3. Editing migrations after applying them. Once 0042_add_field.py has been applied to staging, modifying it diverges your local state from staging's django_migrations table. Create 0043_fix_field.py instead.
4. Forgetting on_delete on ForeignKey. Django requires you to pick. The right answer depends on semantics — CASCADE for "child rows don't exist without the parent", PROTECT to refuse deletion, SET_NULL for "orphan but keep". There is no good default; think about it.
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 want model overhead — much faster than instantiating 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 — it needs a @property. Storing derived data means keeping 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 fires a query per task. With the prefetch, the data is already in memory and Django serves 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 %}
—
{% 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
Two queries instead of N+1. Scale that to a page of 100 tasks and you're saving 99 round-trips — the difference between a 50ms response and a 2-second one.
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; iterating does. Each chain call returns a new QuerySet.
- F expressions for atomic updates that depend on current values; Q objects for OR and complex boolean filters.
aggregatecollapses;annotatedecorates each row.Count,Sum,Avg,Maxare your friends for dashboards.- N+1 is the defining ORM trap.
select_relatedfor FK (one JOIN);prefetch_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 abuse — 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.