RizTech Academy logo
RizTech Academy
The ORM in DepthLesson 3 of 630 min

Custom managers and querysets: naming your queries

As an application grows, the same filters appear again and again — "only booked appointments", "patients in this city", "reports from the last week". Copy-pasting filter(status="booked") across a dozen views is how query logic drifts out of sync and bugs creep in. Custom querysets and managers let you give a query a name and define it once, so Appointment.objects.booked() reads like English and lives in exactly one place. This lesson is how, and it quietly sets up the repository pattern the architecture module returns to.

The problem: the same filter, everywhere

Without custom querysets, this scatters through your codebase:

# in a view
Appointment.objects.filter(status="booked")
# in another view
Appointment.objects.filter(status="booked").filter(patient__city="Pune")
# in a report
Appointment.objects.filter(status="booked")

Three problems. The string "booked" is repeated (a typo in one place is a silent bug). The meaning — "what counts as a booked appointment" — is implicit and duplicated. And if the definition ever changes (say booked-and-not-cancelled), you must find and fix every copy. The query has no name and no home.

The solution: a custom QuerySet with named methods

Define a QuerySet subclass whose methods are your named queries, and attach it to the model as its manager:

from django.db import models


class AppointmentQuerySet(models.QuerySet):
    def booked(self):
        return self.filter(status=Appointment.Status.BOOKED)

    def for_city(self, city):
        return self.filter(patient__city=city)


class Appointment(models.Model):
    # ... fields ...
    objects = AppointmentQuerySet.as_manager()

Now the query has a name and one definition:

Appointment.objects.booked()                # verified count: 3
Appointment.objects.for_city("Pune")        # verified count: 3

AppointmentQuerySet.as_manager() turns the queryset class into the model's objects manager, so its methods are available directly on Appointment.objects. The definition of "booked" now lives in one place; change it there and every caller updates.

The killer feature: named queries chain

Because these methods return querysets, they chain — with each other and with the ordinary queryset methods:

Appointment.objects.booked().for_city("Pune")           # booked AND in Pune
Appointment.objects.booked().order_by("-scheduled_for")  # booked, newest first
Appointment.objects.for_city("Pune").count()             # still just a queryset

booked().for_city("Pune") reads almost like a sentence, and it is still lazy — one SQL query when evaluated. This is why you define the query logic on a QuerySet and use .as_manager(), rather than putting methods on a plain Manager: manager methods do not chain (a method on a Manager returns whatever it returns and the chain stops), but queryset methods return querysets, so they compose endlessly. The chainability is the entire point.

get_queryset for a default filter

Sometimes you want a manager that always applies a filter — a common case is soft-delete, where "deleted" rows should never appear by default. Override get_queryset:

class ActivePatientManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(is_active=True)


class Patient(models.Model):
    is_active = models.BooleanField(default=True)
    objects = models.Manager()          # the default, sees everything
    active = ActivePatientManager()     # only active patients

Now Patient.active.all() returns only active patients, while Patient.objects.all() still sees every row. A word of caution the documentation itself gives: do not make your default manager filter rows out, because the admin, related lookups and other machinery use the default manager and would then silently miss data. Keep objects complete and add a second, named manager for the filtered view — as above.

Where business-query logic belongs

This is the first concrete answer to "where does logic live?" — a theme the best-practices and architecture modules develop. Query logic — the definitions of your meaningful sets of rows — belongs on the model's queryset/manager, not copy-pasted into views. A view should say Appointment.objects.booked(), not spell out filter(status="booked"); the view expresses intent, and the manager holds the definition. This keeps views thin and readable, centralises the query rules, and means a change to what "booked" means is a one-line edit in one file. It is also, essentially, the repository pattern — a named, central place that knows how to fetch your domain objects — arrived at the idiomatic Django way rather than bolted on, which is exactly the point the architecture module will make.

Check your work

The problem custom querysets solve. The same filter(...) scattered across views — repeated strings, implicit duplicated meaning, and no single place to change the definition.

How to define named queries. A models.QuerySet subclass with methods that return self.filter(...), attached via objects = MyQuerySet.as_manager().

Why on a QuerySet, not a Manager. Queryset methods return querysets, so they chain (booked().for_city("Pune")); plain manager methods do not chain. Verified: .booked() → 3, .for_city → 3.

What get_queryset does. Overriding it makes a manager always apply a filter (e.g. only active rows) — but never on the default manager, or the admin and related lookups silently lose data; add a second named manager instead.

Where query logic belongs. On the model's queryset/manager, named once — views express intent (booked()), the manager holds the definition. This is the repository pattern, done the Django way.

Practice

  1. Add an AppointmentQuerySet with booked() and for_city(city); attach it with .as_manager(). Reproduce the counts (3 and 3).
  2. Chain them: Appointment.objects.booked().for_city("Pune"); confirm it works and, via query logging, is one query.
  3. Add a manager method (on a Manager, not a QuerySet) and try to chain a queryset method after it; observe that it does not chain. Move the logic to the queryset and confirm it now does.
  4. Add an is_active field and an active manager overriding get_queryset; confirm Patient.active.all() excludes inactive rows while Patient.objects.all() includes them.
  5. Make the default manager filter out inactive rows, then open the admin and note data going missing; revert and explain why the default manager must stay complete.
  6. Replace a scattered filter(status="booked") in a view with Appointment.objects.booked() and reason about what changes when the definition of "booked" later changes.

Official documentation

Next: transactions, atomicity and database constraints.

Stuck on this lesson?

Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.

About the internship