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