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 indexedindexes = [models.Index(fields=["status", "-created"])]constraints:UniqueConstraint(fields=["author", "slug"], name="...")andCheckConstraint(condition=Q(views__gte=0), name="..."), enforced by the database and byfull_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().