Django’s default objects manager is fine for most things, but it’s not the only way to query your models, and often, it’s not the best way.
Let’s say you have a Product model and you frequently need to find all products that are currently available for sale.
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=255)
price = models.DecimalField(max_digits=10, decimal_places=2)
is_available = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
The common, but slightly repetitive, way to get available products is:
available_products = Product.objects.filter(is_available=True)
This works, but imagine you have several such common queries: products that are on sale, products created in the last week, products with a price above a certain threshold. You’d end up with a lot of .filter() calls scattered throughout your codebase.
This is where custom model managers shine. A model manager is an interface through which you can perform database queries for a model. By default, Django provides models.Manager. We can subclass this to add our own query methods.
Here’s how you’d create a custom manager for our Product model:
class ProductManager(models.Manager):
def available(self):
return self.filter(is_available=True)
def for_sale(self):
# Assuming a 'discount_price' field exists and is less than 'price'
return self.filter(discount_price__lt=models.F('price'))
def recent(self):
from django.utils import timezone
one_week_ago = timezone.now() - timezone.timedelta(days=7)
return self.filter(created_at__gte=one_week_ago)
class Product(models.Model):
name = models.CharField(max_length=255)
price = models.DecimalField(max_digits=10, decimal_places=2)
discount_price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True)
is_available = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
objects = models.Manager() # Keep the default manager
available_products = ProductManager() # Our custom manager
def __str__(self):
return self.name
Notice that we’re keeping the default objects manager and adding a new one, available_products. This is a common pattern, allowing you to use both the standard queries and your custom ones.
Now, querying becomes much cleaner:
available_products = Product.available_products.available()
products_on_sale = Product.available_products.for_sale()
recent_products = Product.available_products.recent()
This is more readable and self-documenting. Instead of reading a .filter(is_available=True) and then inferring its purpose, you read Product.available_products.available(), which immediately tells you what you’re getting.
Beyond just making queries more readable, custom managers are powerful for encapsulating complex logic. Consider a scenario where "available" isn’t just about a flag, but also about stock levels, an expiration date, or even a complex business rule. All that logic can live within the manager method, keeping your views and other parts of your application DRY (Don’t Repeat Yourself).
You can also set a custom manager as the default manager for the model by assigning it to objects:
class ProductManager(models.Manager):
def available(self):
return self.filter(is_available=True)
class Product(models.Model):
name = models.CharField(max_length=255)
price = models.DecimalField(max_digits=10, decimal_places=2)
is_available = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
objects = ProductManager() # Now Product.objects.available() works directly
def __str__(self):
return self.name
In this case, Product.objects.available() would directly use your custom manager. However, it’s often good practice to keep the default objects manager and create a separate custom manager, as shown in the previous example, to preserve access to all the standard manager methods (like all(), get(), filter(), create(), etc.) without explicitly defining them in your custom manager.
When you add custom manager methods, they operate on the queryset returned by the manager. So, self.filter(is_available=True) inside ProductManager.available means "filter the queryset that this manager is currently operating on, based on is_available=True". If you’ve set a custom manager as the default (objects = ProductManager()), then Product.objects.available() first gets the default queryset (which is Product.objects.all()) and then applies the .filter(is_available=True) to it.
One of the most potent aspects of custom managers is their ability to define default querysets. If you want all queries for a model to automatically include certain filters, you can override the get_queryset method.
class ProductManager(models.Manager):
def get_queryset(self):
return super().get_queryset().filter(is_available=True)
class Product(models.Model):
name = models.CharField(max_length=255)
price = models.DecimalField(max_digits=10, decimal_places=2)
is_available = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
objects = ProductManager() # Now Product.objects.all() will ONLY return available products
def __str__(self):
return self.name
Now, Product.objects.all() will inherently only return products where is_available is True. This is a powerful way to enforce business logic at the model level. However, it also means you lose the ability to easily query for all products, including those that are not available, without creating a separate manager for that purpose. This is why explicitly named custom managers (like available_products) are often preferred for clarity and flexibility.
The next logical step is to explore how to use custom managers with Django’s template tags and other reusable components.