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
- Add an
AppointmentQuerySetwithbooked()andfor_city(city); attach it with.as_manager(). Reproduce the counts (3 and 3). - Chain them:
Appointment.objects.booked().for_city("Pune"); confirm it works and, via query logging, is one query. - Add a manager method (on a
Manager, not aQuerySet) 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. - Add an
is_activefield and anactivemanager overridingget_queryset; confirmPatient.active.all()excludes inactive rows whilePatient.objects.all()includes them. - 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.
- Replace a scattered
filter(status="booked")in a view withAppointment.objects.booked()and reason about what changes when the definition of "booked" later changes.
Official documentation
- Django — Managers — Custom managers,
get_queryset, and multiple managers. - Django — Creating a manager with QuerySet methods —
QuerySet.as_manager()and why it chains. - Django —
from_queryset— Combining a custom manager and queryset.
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