Django Custom Managers and Chainable QuerySets
Build custom managers and chainable QuerySets in Django to encapsulate query logic, keep views thin, and write expressive ORM code.
What you'll learn
- ✓The difference between a Manager and a QuerySet in Django
- ✓How to write custom manager methods for common queries
- ✓How to create chainable QuerySet methods with QuerySet.as_manager()
- ✓How to replace the default manager safely
- ✓Real-world patterns for soft deletes, published content, and multi-tenancy
Prerequisites
- •Django models and basic ORM usage
- •Understanding of QuerySet chaining (filter, exclude, annotate)
Every Django model gets a default manager called objects. When you write Post.objects.filter(status="published"), objects is the manager — a gateway to the QuerySet API. But scattering .filter(status="published") across fifty views creates maintenance debt. Custom managers and QuerySets let you encapsulate query logic once and chain it everywhere.
Managers vs QuerySets
A Manager is the interface between a model class and the database. It returns QuerySets. A QuerySet is a lazy, chainable representation of a SQL query.
The default manager (objects) provides methods like all(), filter(), create(), and get(). Custom managers add your own methods to this interface. Custom QuerySets let those methods chain with each other.
Writing a Custom Manager
The simplest approach: subclass models.Manager and add methods.
# blog/managers.py
from django.db import models
class PostManager(models.Manager):
def published(self):
return self.filter(status="published", published_at__isnull=False)
def drafts(self):
return self.filter(status="draft")
def by_author(self, user):
return self.filter(author=user)
Attach it to the model:
# blog/models.py
from django.db import models
from .managers import PostManager
class Post(models.Model):
title = models.CharField(max_length=200)
status = models.CharField(max_length=20, default="draft")
published_at = models.DateTimeField(null=True, blank=True)
author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
objects = PostManager()
def __str__(self):
return self.title
Now you can write:
Post.objects.published()
Post.objects.drafts()
Post.objects.by_author(request.user)
The Chaining Problem
Manager methods return QuerySets, but you can’t chain custom methods with each other:
# This works
Post.objects.published().order_by("-published_at")
# This FAILS — .published() returns a plain QuerySet, not a PostManager
Post.objects.by_author(request.user).published()
The returned QuerySet doesn’t know about your custom published() method. This is the exact problem custom QuerySets solve.
Custom QuerySets for Chainable Methods
Move your methods to a QuerySet subclass instead:
# blog/querysets.py
from django.db import models
class PostQuerySet(models.QuerySet):
def published(self):
return self.filter(status="published", published_at__isnull=False)
def drafts(self):
return self.filter(status="draft")
def by_author(self, user):
return self.filter(author=user)
def recent(self, limit=10):
return self.order_by("-published_at")[:limit]
Use as_manager() to create a manager from the QuerySet:
# blog/models.py
from django.db import models
from .querysets import PostQuerySet
class Post(models.Model):
title = models.CharField(max_length=200)
status = models.CharField(max_length=20, default="draft")
published_at = models.DateTimeField(null=True, blank=True)
author = models.ForeignKey("auth.User", on_delete=models.CASCADE)
objects = PostQuerySet.as_manager()
def __str__(self):
return self.title
Now every method chains:
# All of these work
Post.objects.published()
Post.objects.by_author(request.user).published()
Post.objects.published().by_author(request.user).order_by("-published_at")
Post.objects.drafts().by_author(request.user)
Each custom method returns a refined QuerySet that still has your custom methods available.
Combining a Custom Manager with a Custom QuerySet
Sometimes you need methods on the manager that don’t belong on a QuerySet — like a method that returns a non-QuerySet value. Use Manager.from_queryset():
# blog/managers.py
from django.db import models
from .querysets import PostQuerySet
class PostManager(models.Manager):
def get_queryset(self):
return PostQuerySet(self.model, using=self._db)
def published_count(self):
"""Returns an integer, not a QuerySet — belongs on Manager only."""
return self.get_queryset().published().count()
def most_recent_published(self):
"""Convenience method that returns a single object."""
return self.get_queryset().published().order_by("-published_at").first()
# Alternative: auto-generate a manager from the QuerySet
PostManager = PostManager.from_queryset(PostQuerySet)
With from_queryset(), all QuerySet methods are automatically available on the manager, plus your custom manager-only methods.
Pattern: Soft Deletes with a Custom Manager
Soft deletes are the classic use case. Instead of removing rows, you set a deleted_at timestamp and filter them out by default.
# core/querysets.py
from django.db import models
from django.utils import timezone
class SoftDeleteQuerySet(models.QuerySet):
def delete(self):
"""Soft delete — set deleted_at instead of removing rows."""
return self.update(deleted_at=timezone.now())
def hard_delete(self):
"""Actually remove rows from the database."""
return super().delete()
def alive(self):
return self.filter(deleted_at__isnull=True)
def dead(self):
return self.filter(deleted_at__isnull=False)
class SoftDeleteManager(models.Manager):
def get_queryset(self):
"""Default queryset excludes soft-deleted rows."""
return SoftDeleteQuerySet(self.model, using=self._db).alive()
class AllObjectsManager(models.Manager):
"""Includes soft-deleted rows — use for admin or recovery."""
def get_queryset(self):
return SoftDeleteQuerySet(self.model, using=self._db)
# core/models.py
from django.db import models
class SoftDeleteModel(models.Model):
deleted_at = models.DateTimeField(null=True, blank=True)
objects = SoftDeleteManager()
all_objects = AllObjectsManager()
class Meta:
abstract = True
def delete(self, using=None, keep_parents=False):
from django.utils import timezone
self.deleted_at = timezone.now()
self.save(update_fields=["deleted_at"])
def hard_delete(self):
super().delete()
Any model that inherits SoftDeleteModel now gets soft deletes by default:
class Article(SoftDeleteModel):
title = models.CharField(max_length=200)
# Usage
article.delete() # Sets deleted_at, keeps the row
Article.objects.all() # Only alive articles
Article.all_objects.all() # Everything, including soft-deleted
Article.all_objects.dead() # Only soft-deleted articles
article.hard_delete() # Actually removes the row
Pattern: Annotated QuerySets
Custom QuerySet methods can add annotations for computed fields:
class ProductQuerySet(models.QuerySet):
def with_review_stats(self):
return self.annotate(
avg_rating=models.Avg("reviews__rating"),
review_count=models.Count("reviews"),
)
def with_total_revenue(self):
return self.annotate(
total_revenue=models.Sum(
models.F("orderitems__quantity") * models.F("orderitems__unit_price")
)
)
def bestsellers(self, min_orders=100):
return self.annotate(
order_count=models.Count("orderitems")
).filter(order_count__gte=min_orders)
# In views — clean and readable
products = (
Product.objects
.with_review_stats()
.with_total_revenue()
.filter(avg_rating__gte=4.0)
.order_by("-total_revenue")
)
Best Practices
Name methods as filters, not actions. Use published() not get_published(). QuerySet methods refine the query — they should read like adjectives.
Return self.filter() or self.exclude(), not self.all().filter(). Returning self.filter(...) preserves the chain naturally.
Don’t put business logic in managers. Managers encapsulate query logic. Business rules like “can this user publish?” belong in model methods or service functions.
Be careful overriding get_queryset(). If your default manager filters out rows (like soft deletes), Django admin, related managers, and generic views all use it. Provide a second manager (all_objects) for admin use and set class Meta: default_manager_name if needed.
Use abstract models to share managers. If multiple models need the same QuerySet (like soft deletes or multi-tenancy filtering), put the manager on an abstract base model.
Summary
Custom managers and QuerySets move query logic out of views and into a single, testable location. Use QuerySet subclasses with as_manager() for chainable methods. Use Manager.from_queryset() when you also need manager-only methods. Apply patterns like soft deletes and annotated QuerySets through abstract base models to keep your codebase DRY and your views thin.
Related articles
- Django Django + Celery: Background Tasks and Task Queues
Integrate Celery with Django for background task processing — setup, writing tasks, retries, periodic tasks, and monitoring with Flower.
- Django Django Database Optimization: select_related, prefetch_related, and Beyond
Eliminate N+1 queries in Django with select_related, prefetch_related, Prefetch objects, annotations, and query profiling tools.
- Django Writing Custom Middleware in Django
Build custom Django middleware — request/response processing, exception handling, middleware ordering, and real-world examples.
- Django Django Multi-Tenancy: Shared Database Schema Patterns
Implement multi-tenancy in Django using shared database schemas — tenant models, middleware, filtered QuerySets, and data isolation.