PythonMastery
intermediate 26 min read · lesson 10 of 12 in Web Frameworks

Django Models & ORM

1 · The lesson

read

Many 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 django and python manage.py runserver. Use python manage.py shell for the interactive ORM examples below.


1. A Model Is a Python Class That Maps to a Table

python
# 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.Model and each attribute is a field. Django uses the fields to generate a CREATE TABLE.
  • An id column 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_delete is required. Pick CASCADE (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), or SET_DEFAULT.
  • related_name="articles" gives you the reverse accessor: user.articles.all() fetches all of a user's articles. Without it, you'd write user.article_set.all().
  • Meta holds the per-model config: ordering, indexes, db_table, verbose_name, unique_together, constraints. The Meta.ordering you 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:

FieldSQL typeWhen
CharField(max_length=N)VARCHAR(N)Short strings: titles, names, slugs
TextField()TEXTText of any length: bodies, descriptions
IntegerField()INTEGERCounters, IDs, ages
BigIntegerField()BIGINTWhen INTEGER might overflow (~2B)
FloatField() / DecimalField(max_digits, decimal_places)FLOAT / NUMERICFloats for science, Decimal for money
BooleanField()BOOLEANFlags. Use default=False, not null=True
DateField() / DateTimeField()DATE / TIMESTAMPauto_now_add=True sets on create, auto_now=True updates on every save
EmailField() / URLField() / SlugField()VARCHAR with validatorsValidated CharField variants
UUIDField(default=uuid.uuid4)UUIDWhen you want non-sequential primary keys
JSONField()JSONB (Postgres) / TEXTSemi-structured data: config, properties
ForeignKey(M, on_delete=...)INTEGER REFERENCES ...Many-to-one relationship
OneToOneField(M, on_delete=...)FK + UNIQUEProfile-extends-User patterns
ManyToManyField(M)Through tableTags on articles, members in groups

Two flags every field accepts:

  • null=True: the column accepts NULL in 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.

bash
# 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 0040 collapses 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).
bash
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:

python
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):

python
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:

python
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:

python
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.

python
# 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:

python
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:

python
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:

python
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.
python
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.

python
# 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.

python
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.

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:

python
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:

python
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:

python
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:

python
@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.

python
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:

python
# 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
python
# 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"
    ...
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:

bash
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 are ManyToManyField, 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
python
# 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
python
# 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})
python
# 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"),
]
html
{# 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:

bash
python manage.py makemigrations tasks
python manage.py migrate

Verify in the shell:

python
$ 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__. The id PK 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.
  • aggregate collapses, and annotate adds a value to each row. Count, Sum, Avg and Max are useful for dashboards.
  • N+1 is the most typical ORM mistake. select_related for FK (one JOIN), and prefetch_related for M2M (second query, joined in Python).
  • Transactions with @transaction.atomic or with 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.