Opened 10 years ago

Last modified 8 days 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: Triage Stage: Accepted
Has patch: no Needs documentation: no
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.

Change History (4)

comment:1 by Loic Bistuer, 10 years ago

It's worth noting that QuerySet.__init__() can't be used for providing such initialization/customization:

  • QuerySet are often cloned, and the customization should only ever apply once.
  • QuerySet methods return a cloned QuerySet instance and __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

comment:2 by Tim Graham, 10 years ago

Triage Stage: UnreviewedAccepted

comment:3 by Mariusz Felisiak, 8 days ago

Owner: changed from nobody to Mariusz Felisiak
Status: newassigned

comment:4 by Simon Charette, 8 days 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.

Last edited 8 days ago by Simon Charette (previous) (diff)
Note: See TracTickets for help on using tickets.
Back to Top