Custom Managers & QuerySets

Give your models a fluent, chainable, domain-specific query API using QuerySet.as_manager().

The single biggest readability win in a mature Django codebase is moving query logic off your views and onto your models. Instead of Post.objects.filter(status='published', published_at__lte=now()) scattered in fifteen places, you write Post.objects.published() once.

Manager vs QuerySet — know the difference

  • A Manager is the entry point (Post.objects). Its methods are not chainable onto results.
  • A QuerySet is chainable. Methods you add here compose: .published().popular().recent().

The modern idiom is to write one custom QuerySet, add your methods there, and expose it as the manager with .as_manager(). You get chainability everywhere for free.

from django.db import models

class PostQuerySet(models.QuerySet):
    def published(self):
        return self.filter(status='published')

    def by(self, author):
        return self.filter(author=author)

class Post(models.Model):
    status = models.CharField(max_length=20)
    objects = PostQuerySet.as_manager()

# Chainable, reads like English:
# Post.objects.published().by(request.user)

Note the gotcha: methods on a custom Manager only work off Post.objects.x(), not off a queryset. Methods on a custom QuerySet exposed via as_manager() work in both positions. That is why as_manager() is the senior default.

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(self, author):
        return self.filter(author=author)

    def with_comment_counts(self):
        return self.annotate(num_comments=models.Count('comments'))

    def search(self, term):
        return self.filter(
            models.Q(title__icontains=term) | models.Q(body__icontains=term)
        )


class ArticleManager(models.Manager.from_queryset(ArticleQuerySet)):
    # Manager-only helper: not something you chain onto a queryset.
    def create_draft(self, author, **kwargs):
        return self.create(author=author, status='draft', **kwargs)


class Article(models.Model):
    STATUS = [('draft', 'Draft'), ('published', 'Published')]
    title = models.CharField(max_length=200)
    body = models.TextField()
    status = models.CharField(max_length=20, choices=STATUS, default='draft')
    published_at = models.DateTimeField(null=True, blank=True)
    author = models.ForeignKey('auth.User', on_delete=models.CASCADE)

    objects = ArticleManager()

# Everything composes cleanly in a view or service layer:
# (Article.objects
#     .published()
#     .by(request.user)
#     .search('django')
#     .with_comment_counts()
#     .order_by('-num_comments'))
#
# And the manager-only helper still works:
# Article.objects.create_draft(author=request.user, title='WIP')

When to use it

  • A developer writes a PublishedManager to add a .published() method on Post so views don't repeat the filter logic.
  • A team creates a custom QuerySet with chainable methods like .active() and .by_author(user) for reusable filtering.
  • A developer uses as_manager() to expose custom queryset methods directly on the model's manager.

More examples

Custom Manager with filter

Defines a second manager on Post that pre-filters to non-draft posts for cleaner view queries.

Example · python
from django.db import models

class PublishedManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(draft=False)

class Post(models.Model):
    title = models.CharField(max_length=250)
    draft = models.BooleanField(default=True)

    objects = models.Manager()     # default manager
    published = PublishedManager() # custom manager

# Usage: Post.published.all()

Custom QuerySet with chainable methods

Exposes chainable filter methods directly on the manager so queries read like natural language.

Example · python
class PostQuerySet(models.QuerySet):
    def published(self):
        return self.filter(draft=False)

    def by_author(self, user):
        return self.filter(author=user)

class Post(models.Model):
    title = models.CharField(max_length=250)
    objects = PostQuerySet.as_manager()

# Usage: Post.objects.published().by_author(user)

Manager used in admin

Overrides get_queryset() in a ModelAdmin to scope the admin list view to the custom manager.

Example · python
class PostAdmin(admin.ModelAdmin):
    def get_queryset(self, request):
        # Use the custom manager in admin list
        return Post.published.all()

admin.site.register(Post, PostAdmin)

Discussion

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