Opened 11 years ago
Last modified 3 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
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()
Compare that with
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()
follow-up: 6 comment:5 by , 3 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 , 3 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 , 3 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 , 3 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