按Django中相关模型的自定义QuerySet过滤

Bar*_*Naz 5 django django-queryset

假设我有两个模型:BookAuthor

class Author(models.Model):
    name = models.CharField()
    country = models.CharField()
    approved = models.BooleanField()


class Book(models.Model):
    title = models.CharField()
    approved = models.BooleanField()
    author = models.ForeignKey(Author)
Run Code Online (Sandbox Code Playgroud)

这两个模型中的每一个都有一个approved属性,用于在网站上显示或隐藏对象。如果Book未获批准,则将其隐藏。如果Author不批准,他所有的书都会被隐藏起来。

为了以 DRY 方式定义这些标准,制作自定义 QuerySet 看起来是一个完美的解决方案:

class AuthorQuerySet(models.query.QuerySet):
    def for_site():
        return self.filter(approved=True)

class BookQuerySet(models.query.QuerySet):
    def for_site():
        reuturn self.filter(approved=True).filter(author__approved=True)
Run Code Online (Sandbox Code Playgroud)

将这些 QuerySets 连接到相应的模型后,可以像这样查询它们:Book.objects.for_site(),无需每次都对所有过滤进行硬编码。


尽管如此,这个解决方案仍然不完美。稍后我可以决定向作者添加另一个过滤器:

class AuthorQuerySet(models.query.QuerySet):
    def for_site():
        return self.filter(approved=True).exclude(country='Problematic Country')
Run Code Online (Sandbox Code Playgroud)

但是这个新过滤器只能在 中使用Author.objects.for_site(),而不能在 中使用Book.objects.for_site(),因为它是硬编码的。


所以我的问题是:是否可以在过滤不同模型时应用相关模型的自定义查询集,使其看起来类似于:

class BookQuerySet(models.query.QuerySet):
    def for_site():
        reuturn self.filter(approved=True).filter(author__for_site=True)
Run Code Online (Sandbox Code Playgroud)

哪里for_siteAuthor模型的自定义 QuerySet 。

Bar*_*Naz 1

我想,我已经提出了一个基于Q对象的解决方案,官方文档中对此进行了描述。这绝对不是人们能发明的最优雅的解决方案,但它确实有效。请参阅下面的代码。

\n\n
from django.db import models\nfrom django.db.models import Q\n\n\n######## Custom querysets\nclass QuerySetRelated(models.query.QuerySet):\n    """Queryset that can be applied in filters on related models"""\n\n    @classmethod\n    def _qq(cls, q, related_name):\n        """Returns a Q object or a QuerySet filtered with the Q object, prepending fields with the related_name if specified"""\n        if not related_name:\n            # Returning Q object without changes\n            return q\n        # Recursively updating keywords in this and nested Q objects\n        for i_child in range(len(q.children)):\n            child = q.children[i_child]\n            if isinstance(child, Q):\n                q.children[i_child] = cls._qq(child, related_name)\n            else:\n                q.children[i_child] = (\'__\'.join([related_name, child[0]]), child[1])\n        return q\n\n\nclass AuthorQuerySet(QuerySetRelated):\n\n    @classmethod\n    def for_site_q(cls, q_prefix=None):\n        q = Q(approved=True)\n        q = q & ~Q(country=\'Problematic Country\')\n        return cls._qq(q, q_prefix)\n\n\n    def for_site(self):\n        return self.filter(self.for_site_q())\n\n\nclass BookQuerySet(QuerySetRelated):\n\n    @classmethod\n    def for_site_q(cls, q_prefix=None):\n        q = Q(approved=True) & AuthorQuerySet.for_site_q(\'author\')\n        return cls._qq(q, q_prefix)\n\n\n    def for_site(self):\n        return self.filter(self.for_site_q())\n\n\n\n######## Models\nclass Author(models.Model):\n    name = models.CharField(max_length=255)\n    country = models.CharField(max_length=255)\n    approved = models.BooleanField()\n\n    objects = AuthorQuerySet.as_manager()\n\n\nclass Book(models.Model):\n    title = models.CharField(max_length=255)\n    approved = models.BooleanField()\n    author = models.ForeignKey(Author)\n\n    objects = BookQuerySet.as_manager()\n
Run Code Online (Sandbox Code Playgroud)\n\n

这样,每当AuthorQuerySet.for_site_q()方法发生更改时,都会自动反映在BookQuerySet.for_site()方法中。

\n\n

这里,自定义QuerySet类通过组合不同的对象来在类级别执行选择Q,而不是在对象级别使用filter()或方法。exclude()拥有一个Q对象可以有 3 种不同的使用方式:

\n\n
    \n
  1. 将其放在调用中filter(),以过滤查询集;
  2. \n
  3. Q使用& (AND)或运算符将其与其他对象组合| (OR)
  4. \n
  5. Q通过访问其children属性(在超类中定义)动态更改对象中使用的关键字名称django.utils.tree.Node
  6. \n
\n\n

_qq()每个自定义类中定义的方法负责QuerySet将指定的值添加related_name到所有过滤器键的前面。

\n\n

如果我们有一个q = Q(approved=True)对象,那么我们可以有以下输出:

\n\n
    \n
  1. self._qq(q)\xe2\x80\x93 相当于self.filter(approved=True);
  2. \n
  3. self._qq(q, \'author\')\xe2\x80\x93 相当于self.filter(author__approved=True)
  4. \n
\n\n
\n\n

该解决方案仍然存在严重的缺陷:

\n\n
    \n
  1. 必须显式导入并调用QuerySet相关模型的自定义类;
  2. \n
  3. 对于每个过滤器方法,必须定义两个方法filter_q(类方法)和filter(实例方法);
  4. \n
\n\n
\n\n

更新:2.通过动态创建过滤器方法可以部分减少该缺点:

\n\n
# in class QuerySetRelated\n    @classmethod\n    def add_filters(cls, names):\n        for name in names:\n            method_q = getattr(cls, \'{0:s}_q\'.format(name))\n            def function(self, *args, **kwargs):\n                return self.filter(method_q(*args, **kwargs))\n            setattr(cls, name, function)\n\nAuthorQuerySet.add_filters([\'for_site\'])\nBookQuerySet.add_filters([\'for_site\'])\n
Run Code Online (Sandbox Code Playgroud)\n\n
\n\n

因此,如果有人提出更优雅的解决方案,请提出。我们将非常感激。

\n