Opened 11 years ago
Last modified 2 weeks ago
#23533 assigned New feature
Hook for default QuerySet filtering defined on the QuerySet itself.
| Reported by: | Loic Bistuer | Owned by: | Mariusz Felisiak |
|---|---|---|---|
| Component: | Database layer (models, ORM) | Version: | dev |
| Severity: | Normal | Keywords: | |
| Cc: | Simon Charette | Triage Stage: | Accepted |
| Has patch: | yes | Needs documentation: | yes |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
Django 1.7 brought managers automatically created from QuerySet which replaces defining a custom manager for the purpose of defining reusable methods. Refs #20625.
One use-case remains inelegant: using a custom QuerySet with default QuerySet customization/filtering:
BaseCustomManager = Manager.from_queryset(CustomQueryset)
class CustomManager (BaseCustomManager ):
def get_queryset(self):
queryset = super(Manager, self).get_queryset()
return queryset.filter(...)
This ticket proposes adding a hook on QuerySet to enable this without requiring a custom Manager.
Attachments (1)
Change History (10)
comment:1 by , 11 years ago
comment:2 by , 11 years ago
| Triage Stage: | Unreviewed → Accepted |
|---|
comment:3 by , 21 months ago
| Owner: | changed from to |
|---|---|
| Status: | new → assigned |
comment:4 by , 21 months ago
It'd be good to explore other implementations than that get_initial_queryset hook as that bi-directionnaly couples Querysets with managers (even more than the existing as_manager method) and prevents reuse of the same queryset class for different managers of the same model.
Approaches such as CustomQueryset.as_manager(filter=Q(is_active=True)) and CustomQueryset.filter(is_active=True).as_manager() (this would require marking some methods class_or_instance_method at __init_subclass__ time to capture the calls and apply them at Manager.contribute_to_class / app readyness time) seem more valuable as they don't require overriding any methods.
In other words, the get_initial_queryset hook saves you from defining a manager but you still have to define a method. It also ties your queryset class to a single manager usage with seems wrong? What if you want to use your custom queryset class with two different filters sets?
Some code example might help understanding better here.
Today the problem is
class FooQueryset(models.QuerySet): def is_bar(self): return self.filter(bar=True) class FooBazManager(FooQueryset.as_manager()): def get_queryset(self): return super().get_queryset().filter(baz=True) class FooBatQueryset(FooQueryset.as_manager()): ddef get_queryset(self): return super().get_queryset().filter(baz=True) class Foo(models.Model): bar = models.BooleanField() baz = models.BooleanField() bat = models.BooleanField() objects = FooQueryset.as_manager() baz_objects = FooBazQueryset.as_manager() bat_objects = FooBatQueryset.as_manager()
What get_initial_queryset would allow
class FooQueryset(models.QuerySet): def is_bar(self): return self.filter(bar=True) class FooBazQueryset(FooQueryset): def get_initial_queryset(self): return self.filter(baz=True) class FooBatQueryset(FooQueryset): def get_initial_queryset(self): return self.filter(bat=True) class Foo(models.Model): bar = models.BooleanField() baz = models.BooleanField() bat = models.BooleanField() objects = FooQueryset.as_manager() baz_objects = FooBazQueryset.as_manager() bat_objects = FooBatQueryset.as_manager()
And what a more thorough solution would provide
class FooQueryset(models.QuerySet): def is_bar(self): return self.filter(bar=True) class Foo(models.Model): bar = models.BooleanField() baz = models.BooleanField() bat = models.BooleanField() objects = FooQueryset.as_manager() baz_objects = FooQueryset.filter(baz=True).as_manager() bat_objects = FooQueryset.filter(bat=True).as_manager()
I don't believe that get_initial_queryset brings much to the table TBH.
follow-up: 6 comment:5 by , 2 weeks ago
Brilliant idea, thanks Simon. QuerySet.filter() already exist so I think we should use a different name for a class method to avoid confusion, maybe QuerySet.init_filter() or QuerySet.initial_filter().
comment:6 by , 2 weeks ago
Replying to Mariusz Felisiak:
Brilliant idea, thanks Simon.
QuerySet.filter()already exist so I think we should use a different name for a class method to avoid confusion, maybeQuerySet.init_filter()orQuerySet.initial_filter().
Ah ok we can use class_or_instance_method() hook.
comment:7 by , 2 weeks ago
I've created a surprisingly small PoC that works for me (also Django test suite pass with it). Simon, Can you take a look?
I've tried it with the following model:
from django.db import models # Create your models here. class MyModelQuerySet(models.QuerySet): pass class MyModel(models.Model): name = models.TextField() active = models.BooleanField(default=False) code = models.CharField(max_length=255, default="X") active_objects = MyModelQuerySet.filter(active=True).as_manager() nonactive_objects = MyModelQuerySet.filter(active=False, code="X").as_manager()
by , 2 weeks ago
| Attachment: | 0001-Fixed-23533-Allow-defining-initial-filters-on-QueryS.patch added |
|---|
It's worth noting that
QuerySet.__init__()can't be used for providing such initialization/customization:__init__can't return a different instance.So far the best option I can think of is a hook called externally by the manager.
POC with a
QuerySet.get_initial_queryset()method https://github.com/loic/django/compare/ticket23533