PageView

Django Tips #11 Custom Manager With Chainable QuerySets

Django Tips #11 Custom Manager With Chainable QuerySets
Updated at Aug 17, 2016: I've updated the article mentioning the DocumentQuerySet.as_manager() option. Thanks to Darryl and Mark for suggesting in the comments below!

In a Django model, the Manager is the interface that interacts with the database. By default the manager is available through the Model.objects property. The default manager every Django model gets out of the box is the django.db.models.Manager. It is very straightforward to extend it and change the default manager.

from django.db import models

class DocumentManager(models.Manager):
    def pdfs(self):
        return self.filter(file_type='pdf')

    def smaller_than(self, size):
        return self.filter(size__lt=size)

class Document(models.Model):
    name = models.CharField(max_length=30)
    size = models.PositiveIntegerField(default=0)
    file_type = models.CharField(max_length=10, blank=True)

    objects = DocumentManager()

With that you will be able to retrieve all pdf files like this:

Document.objects.pdfs()

The thing is, this method is not chainable. I mean, you can still use order_by or filter in the result:

Document.objects.pdfs().order_by('name')

But if you try to chain the methods it will break:

Document.objects.pdfs().smaller_than(1000)
AttributeError: 'QuerySet' object has no attribute 'smaller_than'

To make it work you must create custom QuerySet methods:

class DocumentQuerySet(models.QuerySet):
    def pdfs(self):
        return self.filter(file_type='pdf')

    def smaller_than(self, size):
        return self.filter(size__lt=size)

class DocumentManager(models.Manager):
    def get_queryset(self):
        return DocumentQuerySet(self.model, using=self._db)  # Important!

    def pdfs(self):
        return self.get_queryset().pdfs()

    def smaller_than(self, size):
        return self.get_queryset().smaller_than(size)

class Document(models.Model):
    name = models.CharField(max_length=30)
    size = models.PositiveIntegerField(default=0)
    file_type = models.CharField(max_length=10, blank=True)

    objects = DocumentManager()

Now you can use it just like any other QuerySet method:

Document.objects.pdfs().smaller_than(1000).exclude(name='Article').order_by('name')

If you are only defining custom QuerySets in the Manager, you can simply extend the models.QuerySet and in the model set the manager as objects = DocumentQuerySet.as_manager():

class DocumentQuerySet(models.QuerySet):
    def pdfs(self):
        return self.filter(file_type='pdf')

    def smaller_than(self, size):
        return self.filter(size__lt=size)

class Document(models.Model):
    name = models.CharField(max_length=30)
    size = models.PositiveIntegerField(default=0)
    file_type = models.CharField(max_length=10, blank=True)

    objects = DocumentQuerySet.as_manager()

You can keep the code inside the models.py. But as the code base grow, I prefer to keep the Managers and QuerySets in a different module, named managers.py.



Sponsored Links