【问题标题】:Filter by custom QuerySet of a related model in Django按 Django 中相关模型的自定义 QuerySet 过滤
【发布时间】:2017-04-26 16:29:04
【问题描述】:

假设我有两个模型: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)

这两个模型中的每一个都有一个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)

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


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

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

但是这个新过滤器只能在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)

其中for_siteAuthor 模型的自定义查询集。

【问题讨论】:

  • 我不确定这是否有帮助,但请尝试this
  • 这正是我所指的将这些查询集连接到相应的模型之后。但我看不出它如何帮助使用该管理器过滤另一个相关模型的实例。也许我错过了什么?
  • 你并没有错过什么,我真的认为这是一个好问题,我认为你不会找到开箱即用的东西。您可以查看docs for custom lookup 并为自己编写代码。

标签: django django-queryset


【解决方案1】:

我想,我想出了一个基于Q 对象的解决方案,这些对象在official documentation 中有描述。这绝对不是人们可以发明的最优雅的解决方案,但它确实有效。请参阅下面的代码。

from django.db import models
from django.db.models import Q


######## Custom querysets
class QuerySetRelated(models.query.QuerySet):
    """Queryset that can be applied in filters on related models"""

    @classmethod
    def _qq(cls, q, related_name):
        """Returns a Q object or a QuerySet filtered with the Q object, prepending fields with the related_name if specified"""
        if not related_name:
            # Returning Q object without changes
            return q
        # Recursively updating keywords in this and nested Q objects
        for i_child in range(len(q.children)):
            child = q.children[i_child]
            if isinstance(child, Q):
                q.children[i_child] = cls._qq(child, related_name)
            else:
                q.children[i_child] = ('__'.join([related_name, child[0]]), child[1])
        return q


class AuthorQuerySet(QuerySetRelated):

    @classmethod
    def for_site_q(cls, q_prefix=None):
        q = Q(approved=True)
        q = q & ~Q(country='Problematic Country')
        return cls._qq(q, q_prefix)


    def for_site(self):
        return self.filter(self.for_site_q())


class BookQuerySet(QuerySetRelated):

    @classmethod
    def for_site_q(cls, q_prefix=None):
        q = Q(approved=True) & AuthorQuerySet.for_site_q('author')
        return cls._qq(q, q_prefix)


    def for_site(self):
        return self.filter(self.for_site_q())



######## Models
class Author(models.Model):
    name = models.CharField(max_length=255)
    country = models.CharField(max_length=255)
    approved = models.BooleanField()

    objects = AuthorQuerySet.as_manager()


class Book(models.Model):
    title = models.CharField(max_length=255)
    approved = models.BooleanField()
    author = models.ForeignKey(Author)

    objects = BookQuerySet.as_manager()

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

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

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

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

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

  1. self._qq(q) - 等同于 self.filter(approved=True);
  2. self._qq(q, 'author') - 相当于self.filter(author__approved=True)

这个解决方案仍然有严重的缺陷:

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

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

# in class QuerySetRelated
    @classmethod
    def add_filters(cls, names):
        for name in names:
            method_q = getattr(cls, '{0:s}_q'.format(name))
            def function(self, *args, **kwargs):
                return self.filter(method_q(*args, **kwargs))
            setattr(cls, name, function)

AuthorQuerySet.add_filters(['for_site'])
BookQuerySet.add_filters(['for_site'])

因此,如果有人提出更优雅的解决方案,请提出建议。将不胜感激。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-09-14
    • 2021-06-21
    • 2017-01-21
    • 2018-05-01
    • 2017-11-17
    • 1970-01-01
    • 2021-11-24
    • 2011-06-04
    相关资源
    最近更新 更多