Custom Managers & QuerySets
Move business-logic queries out of your views and onto the model where they belong, using chainable custom QuerySets and managers.
If you ever catch yourself writing Article.objects.filter(status="published", published_at__lte=timezone.now()) in three different views, that is your model asking for a custom QuerySet. The goal is a fat model, thin view: query logic lives with the data, named in domain language.
QuerySet vs Manager — and why you want both
The trick most tutorials miss: define your methods on a QuerySet subclass, then turn it into a manager with .as_manager(). That way your methods are chainable — Article.objects.published().by_author(ada) just works, because each method returns a QuerySet.
class ArticleQuerySet(models.QuerySet):
def published(self):
return self.filter(status="published")
def by_author(self, user):
return self.filter(author=user)
class Article(models.Model):
objects = ArticleQuerySet.as_manager()When you need a real Manager
Use Manager.from_queryset(ArticleQuerySet) when you also want to override get_queryset() — for example a PublishedManager that hides drafts by default. Keep objects as the unfiltered manager (Django uses the first manager defined as the default for things like the admin and related lookups), and add named managers alongside it.
Example
from django.db import models
from django.utils import timezone
class ArticleQuerySet(models.QuerySet):
def published(self):
return self.filter(status="published", published_at__lte=timezone.now())
def drafts(self):
return self.filter(status="draft")
def by_author(self, user):
return self.filter(author=user)
def with_comment_counts(self):
return self.annotate(num_comments=models.Count("comments"))
class PublishedManager(models.Manager):
"""A manager whose default queryset only ever sees published rows."""
def get_queryset(self):
return ArticleQuerySet(self.model, using=self._db).published()
class Article(models.Model):
STATUS = [("draft", "Draft"), ("published", "Published")]
title = models.CharField(max_length=200)
status = models.CharField(max_length=10, choices=STATUS, default="draft")
published_at = models.DateTimeField(null=True, blank=True)
author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
objects = ArticleQuerySet.as_manager() # default, UNFILTERED (declared first)
live = PublishedManager() # convenience: Article.live.all()
# Chainable, reads like a sentence:
# Article.objects.published().by_author(ada).with_comment_counts()
# Article.live.all() # already limited to publishedWhen to use it
- A developer writes a SoftDeleteManager that filters out deleted records so every view automatically excludes them.
- A team attaches a custom manager to the Category model with a .with_post_count() method for dashboard queries.
- A developer creates a FromEmailManager to scope every query to a specific organization in a multi-tenant app.
More examples
SoftDelete manager hiding deleted rows
Defines a second manager that pre-filters to non-deleted rows, making it safe as the default queryset.
class ActiveManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(deleted_at__isnull=True)
class Post(models.Model):
deleted_at = models.DateTimeField(null=True, blank=True)
objects = models.Manager() # raw access
active = ActiveManager() # hides deleted
# Post.active.all() never returns soft-deleted rowsManager with annotation method
Adds a chainable method to the manager that annotates each category with its post count.
from django.db.models import Count
class CategoryManager(models.Manager):
def with_post_count(self):
return self.annotate(post_count=Count("posts"))
class Category(models.Model):
name = models.CharField(max_length=100)
objects = CategoryManager()
# Category.objects.with_post_count().order_by('-post_count')Pass manager to related managers
as_manager() exposes custom queryset methods on both the direct and reverse related managers.
class PublishedPostQuerySet(models.QuerySet):
def published(self):
return self.filter(draft=False)
class Author(models.Model):
name = models.CharField(max_length=100)
class Post(models.Model):
author = models.ForeignKey(Author, on_delete=models.CASCADE,
related_name="posts")
draft = models.BooleanField(default=True)
objects = PublishedPostQuerySet.as_manager()
# author.posts.published() <- works on the reverse relation too
Discussion