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)

0001-Fixed-23533-Allow-defining-initial-filters-on-QueryS.patch (3.8 KB ) - added by Mariusz Felisiak 2 weeks ago.

Download all attachments as: .zip

Change History (10)

comment:1 by Loic Bistuer, 11 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, 11 years ago

Triage Stage: UnreviewedAccepted

comment:3 by Mariusz Felisiak, 21 months ago

Owner: changed from nobody to Mariusz Felisiak
Status: newassigned

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

Last edited 21 months ago by Simon Charette (previous) (diff)

comment:5 by Mariusz Felisiak, 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().

in reply to:  5 comment:6 by Mariusz Felisiak, 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, maybe QuerySet.init_filter() or QuerySet.initial_filter().

Ah ok we can use class_or_instance_method() hook.

comment:7 by Mariusz Felisiak, 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()
Last edited 2 weeks ago by Mariusz Felisiak (previous) (diff)

comment:8 by Simon Charette, 2 weeks ago

Cc: Simon Charette added

I'll get to it this weekend, thanks for giving it a shot!

comment:9 by Mariusz Felisiak, 2 weeks ago

Has patch: set
Needs documentation: set
Note: See TracTickets for help on using tickets.
Back to Top