如何在不破坏DRY原则的情况下自定义QuerySet和Manager?

78 投票
8 回答
54961 浏览
提问于 2025-04-15 18:38

我正在寻找一种方法,既能实现自定义的 QuerySet,又能实现自定义的 Manager,而不违反“不要重复自己”的原则(DRY)。这是我目前的进展:

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()

这部分运行得很好,直到我做了这样的事情:

inquiries = Inquiry.objects.filter(status=some_status)
my_inquiry_count = inquiries.for_user(request.user).count()

这一下子就把所有东西搞坏了,因为 QuerySetManager 的方法不一样。我尝试创建一个自定义的 QuerySet 类,并在 MyInquiryManager 中实现它,但最后我不得不重复定义所有的方法。

我还发现了这个代码片段,它可以工作,但我需要传递额外的参数给 for_user,所以它就出问题了,因为它过于依赖于重新定义 get_query_set

有没有办法在不重新定义 QuerySetManager 子类中的所有方法的情况下实现这个?

8 个回答

15

你可以通过一个混合类(mixin)来给管理器和查询集提供方法。

这样做还可以避免使用 __getattr__() 这种方式。

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)
71

Django 1.7 版本推出了一种新且简单的方法来创建结合了查询集和模型管理器的功能:

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()

想了解更多细节,可以查看 使用查询集方法创建管理器

60

Django 发生了变化! 在使用这个答案中的代码之前,这段代码是2009年写的,建议你先查看其他答案和Django的文档,看看有没有更合适的解决方案。


我实现这个功能的方法是把实际的 get_active_for_account 添加为一个自定义 QuerySet 的方法。然后,为了让它可以通过管理器使用,你只需要捕捉 __getattr__ 并相应地返回结果。

为了让这个模式可以重复使用,我把 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)

一旦你完成了这一步,在你的模型中,你只需要定义一个自定义的内部类 QuerySet,并把管理器设置为你的自定义管理器:

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)

使用这个模式,以下任何一种方式都可以工作:

>>> Inquiry.objects.active_for_account(user)
>>> Inquiry.objects.all().active_for_account(user)
>>> Inquiry.objects.filter(first_name='John').active_for_account(user)

如果你在使用自定义用户(AbstractUser),你需要把

class CustomQuerySetManager(models.Manager):

改成

from django.contrib.auth.models import UserManager

class CustomQuerySetManager(UserManager):
    ***

撰写回答