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() ...
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
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.