Managers, QuerySet methods and Meta

Django · Practical · 12 min

Named query methods keep business rules like "published" in one place instead of copied filters everywhere.

When the same filter appears in many views, give it a name. Define methods on a custom QuerySet and expose it as the manager:

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

class Post(models.Model):
    objects = PostQuerySet.as_manager()

Now Post.objects.published().filter(author=a) chains naturally. Methods returning QuerySets are composable, which is why defining them on the QuerySet (not only the manager) is the preferred style.

Model methods hold row-level behavior: get_absolute_url() (used by the admin and generic views), computed properties, and domain actions like publish(). A good rule: table-level logic in QuerySets, row-level logic on the model.

Meta options worth knowing:

  • ordering: default ordering, applied to every query, so keep it cheap and indexed
  • indexes = [models.Index(fields=["status", "-created"])]
  • constraints: UniqueConstraint(fields=["author", "slug"], name="...") and CheckConstraint(condition=Q(views__gte=0), name="..."), enforced by the database and by full_clean()
  • verbose_name, verbose_name_plural, db_table, get_latest_by

Database constraints beat Python checks: they hold even for bulk updates, admin edits and other programs writing to the database.

Note that save() does not call full_clean(); validation runs in ModelForms or when you call it explicitly.

Example

from django.db import models
from django.db.models import Q
from django.urls import reverse
from django.utils import timezone
class PostQuerySet(models.QuerySet):
    def published(self):
        return self.filter(status="pub", published_at__lte=timezone.now())
    def by(self, author):
        return self.filter(author=author)
class Post(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField()
    author = models.ForeignKey("Author", on_delete=models.CASCADE, related_name="posts")
    status = models.CharField(max_length=5, default="draft")
    views = models.IntegerField(default=0)
    published_at = models.DateTimeField(null=True, blank=True)
    objects = PostQuerySet.as_manager()
    class Meta:
        ordering = ["-published_at"]
        indexes = [models.Index(fields=["status", "-published_at"])]
        constraints = [models.UniqueConstraint(fields=["author", "slug"], name="unique_author_slug"),
                       models.CheckConstraint(condition=Q(views__gte=0), name="views_gte_0")]
    def get_absolute_url(self):
        return reverse("blog:detail", kwargs={"slug": self.slug})
# Post.objects.published().by(ann)[:10]

Remember

  • Put reusable filters on a QuerySet subclass; expose with as_manager()
  • Row behavior and get_absolute_url() go on the model
  • Declare indexes and constraints in Meta; the database enforces them

Watch out for

  • Expecting model.save() to run field validation; it doesn't call full_clean().

Check yourself

  1. Why define published() on a QuerySet subclass rather than only on a Manager?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.djangoproject.com/en/6.0/topics/db/managers/