通过Django中相关模型的自定义QuerySet进行过滤

时间:2017-04-26 16:29:05

标签: 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)

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

为了以干燥的方式定义这些标准,使自定义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)

将这些QuerysSets连接到相应的模型后,可以像这样查询它们: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模型的自定义QuerySet。

1 个答案:

答案 0 :(得分:0)

我认为,我已经提出了基于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. 使用Q& (AND)运算符将其与其他| (OR)个对象合并;
  3. 通过访问Q属性来动态更改children对象中使用的关键字名称,该属性在超类django.utils.tree.Node中定义
  4. 每个自定义_qq()类中定义的QuerySet方法负责将指定的related_name添加到所有过滤器键。

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

    1. self._qq(q) - 相当于self.filter(approved=True);
    2. self._qq(q, 'author') - 相当于self.filter(author__approved=True)
    3. 这种解决方案仍有严重的缺点:

      1. 必须明确导入并调用相关模型的自定义QuerySet类;
      2. 对于每个过滤方法,必须定义两个方法filter_q(类方法)和filter(实例方法);
      3. 更新:通过动态创建过滤器方法可以部分减少缺陷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'])
        

        因此,如果有人想出更优雅的解决方案,请提出建议。非常感谢。