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)

0001-Fixed-23533-Allow-defining-initial-filters-on-QueryS.patch (3.8 KB ) - added by Mariusz Felisiak 3 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

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()
Version 0, edited 21 months ago by Simon Charette (next)

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

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

Ah ok we can use class_or_instance_method() hook.

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

comment:8 by Simon Charette, 3 weeks ago

Cc: Simon Charette added

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

comment:9 by Mariusz Felisiak, 3 weeks ago

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