Don't filter in Django's qet_queryset
In Django models, it’s easy to customize the queryset that’s returned by a model manager. Say that you’re working on a blog, and you have a model that looks something like this:
class Post(models.Model):
content = models.TextField()
created_at = models.DateTimeField(auto_now_add=True)
You might decide that you always want to return blog posts sorted by most recent. You
can use a custom model manager such that Post.objects.all()
will always include this
ordering by default:
class PostManager(models.Manager):
def get_queryset(self):
return super().get_queryset().order_by("-created_at")
class Post(models.Model):
objects = PostManager()
...
However, overriding get_queryset()
also makes it easy to shoot yourself in the foot.
Perhaps you implement the ability to archive old posts, and you then figure that you’ll make your code a bit
cleaner by only returning those by default:
class PostManager(models.Manager):
def get_queryset(self):
return super().get_queryset()
.filter(archived=False)
.order_by("-created_at")
class Post(models.Model):
objects = PostManager()
unscoped = models.Manager()
archived = models.BooleanField(default=False)
...
Unfortunately, this just made it easy to introduce bugs in the future! Now, whenever a developer
is working with the Post
model, they need to remember to handle two cases: archived posts and un-archived posts.
Say that a year down the line, you need to write a data migration that converts the content of your blog posts from Markdown to HTML. An engineer might write something like:
for post in Post.objects.all():
post.convert_to_html()
This looks fine on the surface, but it will leave all archived posts un-converted. And worse, you’ll only catch this
if you remember to test the code with both archived and non-archived posts, or if you remember to use the unscoped
model
manager.
A good principle is to minimize the number of unusual behaviours that developers need to keep in mind. Custom scopes do the opposite, and add a lot of risk for little payoff.