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

Example · python
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 published

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

Example · python
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 rows

Manager with annotation method

Adds a chainable method to the manager that annotates each category with its post count.

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

Example · python
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

  • Be the first to comment on this lesson.
Custom Managers & QuerySets — Django | SoundsCode