Custom QuerySet and Manager without breaking DRY?
Solution 1
Django has changed! Before using the code in this answer, which was written in 2009, be sure to check out the rest of the answers and the Django documentation to see if there is a more appropriate solution.
The way I've implemented this is by adding the actual get_active_for_account
as a method of a custom QuerySet
. Then, to make it work off the manager, you can simply trap the __getattr__
and return it accordingly
To make this pattern re-usable, I've extracted out the Manager
bits to a separate model manager:
custom_queryset/models.py
from django.db import models
from django.db.models.query import QuerySet
class CustomQuerySetManager(models.Manager):
"""A re-usable Manager to access a custom QuerySet"""
def __getattr__(self, attr, *args):
try:
return getattr(self.__class__, attr, *args)
except AttributeError:
# don't delegate internal methods to the queryset
if attr.startswith('__') and attr.endswith('__'):
raise
return getattr(self.get_query_set(), attr, *args)
def get_query_set(self):
return self.model.QuerySet(self.model, using=self._db)
Once you've got that, on your models all you need to do is define a QuerySet
as a custom inner class and set the manager to your custom manager:
your_app/models.py
from custom_queryset.models import CustomQuerySetManager
from django.db.models.query import QuerySet
class Inquiry(models.Model):
objects = CustomQuerySetManager()
class QuerySet(QuerySet):
def active_for_account(self, account, *args, **kwargs):
return self.filter(account=account, deleted=False, *args, **kwargs)
With this pattern, any of these will work:
>>> Inquiry.objects.active_for_account(user)
>>> Inquiry.objects.all().active_for_account(user)
>>> Inquiry.objects.filter(first_name='John').active_for_account(user)
UPD if you are using it with custom user(AbstractUser
), you need to change
from
class CustomQuerySetManager(models.Manager):
to
from django.contrib.auth.models import UserManager
class CustomQuerySetManager(UserManager):
***
Solution 2
The Django 1.7 released a new and simple way to create combined queryset and model manager:
class InquiryQuerySet(models.QuerySet):
def for_user(self, user):
return self.filter(
Q(assigned_to_user=user) |
Q(assigned_to_group__in=user.groups.all())
)
class Inquiry(models.Model):
objects = InqueryQuerySet.as_manager()
See Creating Manager with QuerySet methods for more details.
Solution 3
You can provide the methods on the manager and queryset using a mixin.
This also avoids the use of a __getattr__()
approach.
from django.db.models.query import QuerySet
class PostMixin(object):
def by_author(self, user):
return self.filter(user=user)
def published(self):
return self.filter(published__lte=datetime.now())
class PostQuerySet(QuerySet, PostMixin):
pass
class PostManager(models.Manager, PostMixin):
def get_query_set(self):
return PostQuerySet(self.model, using=self._db)
Solution 4
You can now use the from_queryset() method on you manager to change its base Queryset.
This allows you to define your Queryset methods and your manager methods only once
from the docs
For advanced usage you might want both a custom Manager and a custom QuerySet. You can do that by calling Manager.from_queryset() which returns a subclass of your base Manager with a copy of the custom QuerySet methods:
class InqueryQueryset(models.Queryset):
def custom_method(self):
""" available on all default querysets"""
class BaseMyInquiryManager(models.Manager):
def for_user(self, user):
return self.get_query_set().filter(
Q(assigned_to_user=user) |
Q(assigned_to_group__in=user.groups.all())
)
MyInquiryManager = BaseInquiryManager.from_queryset(InquiryQueryset)
class Inquiry(models.Model):
ts = models.DateTimeField(auto_now_add=True)
status = models.ForeignKey(InquiryStatus)
assigned_to_user = models.ForeignKey(User, blank=True, null=True)
assigned_to_group = models.ForeignKey(Group, blank=True, null=True)
objects = MyInquiryManager()
Solution 5
A slightly improved version of T. Stone’s approach:
def objects_extra(mixin_class):
class MixinManager(models.Manager, mixin_class):
class MixinQuerySet(QuerySet, mixin_class):
pass
def get_query_set(self):
return self.MixinQuerySet(self.model, using=self._db)
return MixinManager()
Class decorators make usage as simple as:
class SomeModel(models.Model):
...
@objects_extra
class objects:
def filter_by_something_complex(self, whatever parameters):
return self.extra(...)
...
Update: support for nonstandard Manager and QuerySet base classes, e. g. @objects_extra(django.contrib.gis.db.models.GeoManager, django.contrib.gis.db.models.query.GeoQuerySet):
def objects_extra(Manager=django.db.models.Manager, QuerySet=django.db.models.query.QuerySet):
def oe_inner(Mixin, Manager=django.db.models.Manager, QuerySet=django.db.models.query.QuerySet):
class MixinManager(Manager, Mixin):
class MixinQuerySet(QuerySet, Mixin):
pass
def get_query_set(self):
return self.MixinQuerySet(self.model, using=self._db)
return MixinManager()
if issubclass(Manager, django.db.models.Manager):
return lambda Mixin: oe_inner(Mixin, Manager, QuerySet)
else:
return oe_inner(Mixin=Manager)
Comments
-
Jack M. almost 2 years
I'm trying to find a way to implement both a custom
QuerySet
and a customManager
without breaking DRY. This is what I have so far:class MyInquiryManager(models.Manager): def for_user(self, user): return self.get_query_set().filter( Q(assigned_to_user=user) | Q(assigned_to_group__in=user.groups.all()) ) class Inquiry(models.Model): ts = models.DateTimeField(auto_now_add=True) status = models.ForeignKey(InquiryStatus) assigned_to_user = models.ForeignKey(User, blank=True, null=True) assigned_to_group = models.ForeignKey(Group, blank=True, null=True) objects = MyInquiryManager()
This works fine, until I do something like this:
inquiries = Inquiry.objects.filter(status=some_status) my_inquiry_count = inquiries.for_user(request.user).count()
This promptly breaks everything because the
QuerySet
doesn't have the same methods as theManager
. I've tried creating a customQuerySet
class, and implementing it inMyInquiryManager
, but I end up replicating all of my method definitions.I also found this snippet which works, but I need to pass in the extra argument to
for_user
so it breaks down because it relies heavily on redefiningget_query_set
.Is there a way to do this without redefining all of my methods in both the
QuerySet
and theManager
subclasses? -
Jack M. over 14 yearsTry doing a
filter
, then usingget_active_for_account
. It works in your example, but not once you've already used afilter
, and are then working with aQuerySet
, which was my example. -
Sam Saffron over 13 yearscan you decide what should be done with stackoverflow.com/edit-suggestions/1216
-
Sam Saffron over 13 yearsStone you are going to have to edit it in yourself, there is no way to take an edit after it was declined
-
Aneil Mallavarapu about 13 yearsWARNING: I tried this method and discovered that it severely slows down .defer and .only calls.
-
T. Stone over 12 yearsI've updated this response to a community wiki. Those with performance optimizations can adjust the code as necessary.
-
Jelko almost 11 yearsprobably similar to PassThroughManager from pypi.python.org/pypi/django-model-utils
-
Agustín Lado almost 9 yearsThis is the best way to do it, but it'd be got to exemplify how the
for_user
method should take a user and returnself.[...]
to chain together multiple operations. -
Vadim Pushtaev over 8 yearsThat's pretty awesome, they have such decorator in the django itself
-
Vadim Pushtaev over 8 yearsMy Django wants
get_queryset
to be overriden, notget_query_set
. -
Anentropic over 8 yearsFolks, I found a nasty bug in the implementation above, I have edited it with the fix. For explanation of the problem see here gist.github.com/anentropic/0f2d700b5abdc21177bb This must be what @AneilMallavarapu meant when saying it slowed down
defer
andonly
calls... due to pathological extra queries being performed. Fixed now with current edit. -
Barney Szabolcs over 3 yearsFunny how this is not he first answer here. Professionally this is the cleanest solution.
-
Sian Lerk Lau about 3 yearsThe solution provided by @Anentropic is working, though we can improvise it by honoring it's original behavior as per python data model. The outcome will be as follow: gist.github.com/kiawin/13e56e47bde59d9d02112c3d9c373b0d. tl;dr -
__getattr__
only accepts one parameter (we don't need*args
), and it is triggered only if the instance attribute is not found (we don't need to do atry-catch
) -
shahjapan almost 3 yearsI like your solution ! as all methods defined in custom-queryset are available to manager. :-)