【问题标题】:Chaining multiple filter() in Django, is this a bug?在 Django 中链接多个 filter(),这是一个错误吗?
【发布时间】:2011-12-31 04:04:10
【问题描述】:

我一直认为在 Django 中链接多个 filter() 调用总是与在单个调用中收集它们相同。

# Equivalent
Model.objects.filter(foo=1).filter(bar=2)
Model.objects.filter(foo=1,bar=2)

但我在代码中遇到了一个复杂的查询集,但情况并非如此

class Inventory(models.Model):
    book = models.ForeignKey(Book)

class Profile(models.Model):
    user = models.OneToOneField(auth.models.User)
    vacation = models.BooleanField()
    country = models.CharField(max_length=30)

# Not Equivalent!
Book.objects.filter(inventory__user__profile__vacation=False).filter(inventory__user__profile__country='BR')
Book.objects.filter(inventory__user__profile__vacation=False, inventory__user__profile__country='BR')

生成的SQL是

SELECT "library_book"."id", "library_book"."asin", "library_book"."added", "library_book"."updated" FROM "library_book" INNER JOIN "library_inventory" ON ("library_book"."id" = "library_inventory"."book_id") INNER JOIN "auth_user" ON ("library_inventory"."user_id" = "auth_user"."id") INNER JOIN "library_profile" ON ("auth_user"."id" = "library_profile"."user_id") INNER JOIN "library_inventory" T5 ON ("library_book"."id" = T5."book_id") INNER JOIN "auth_user" T6 ON (T5."user_id" = T6."id") INNER JOIN "library_profile" T7 ON (T6."id" = T7."user_id") WHERE ("library_profile"."vacation" = False  AND T7."country" = BR )
SELECT "library_book"."id", "library_book"."asin", "library_book"."added", "library_book"."updated" FROM "library_book" INNER JOIN "library_inventory" ON ("library_book"."id" = "library_inventory"."book_id") INNER JOIN "auth_user" ON ("library_inventory"."user_id" = "auth_user"."id") INNER JOIN "library_profile" ON ("auth_user"."id" = "library_profile"."user_id") WHERE ("library_profile"."vacation" = False  AND "library_profile"."country" = BR )

第一个带有链式filter() 调用的查询集两次有效地在两个条件之间创建一个 OR,而第二个查询集将两个条件与两个条件相加。我期待第一个查询也将 AND 这两个条件。这是预期的行为还是 Django 中的错误?

一个相关问题Is there a downside to using ".filter().filter().filter()..." in Django?的答案似乎表明这两个查询集应该是等价的。

【问题讨论】:

    标签: django django-orm


    【解决方案1】:

    我的理解是它们在设计上略有不同(我当然愿意更正):filter(A, B) 将首先根据 A 过滤,然后根据 B 进行子过滤,而 filter(A).filter(B) 将返回一行匹配 A '和'与 B 匹配的可能不同的行。

    看这里的例子:

    https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships

    特别是:

    单个 filter() 调用中的所有内容都会同时应用以过滤出符合所有这些要求的项目。连续的 filter() 调用进一步限制了对象集

    ...

    在第二个示例 (filter(A).filter(B)) 中,第一个过滤器将查询集限制为 (A)。第二个过滤器将博客集进一步限制为同样属于 (B) 的博客。第二个过滤器选择的条目可能与第一个过滤器中的条目相同,也可能不同。`

    【讨论】:

    • 这种行为虽然有记录,但似乎违反了最小惊讶原则。当字段在同一个模型上时,多个 filter() 的 AND 一起,但当跨越关系时,然后 OR 一起。
    • 我相信你在第一段中有错误的方式 - filter(A, B) 是 AND 情况(文档中的'lennon' AND 2008),而 filter(A).filter (B) 是 OR 情况('lennon' OR 2008)。当您查看问题中生成的查询时,这是有道理的 - .filter(A).filter(B) 案例创建了两次连接,导致 OR。
    • filter(A, B) 是 AND filter(A).filter(B) 是 OR
    • 所以further restrict 表示less restrictive?
    • 这个答案不正确。这不是“或”。这句话“第二个过滤器将博客集进一步限制为那些也是(B)的博客。”清楚地提到“也是(B)”。如果您在此特定示例中观察到类似于 OR 的行为,并不一定意味着您可以概括自己的解释。请看“Kevin 3112”和“Johnny Tsang”的答案。我相信这些都是正确的答案。
    【解决方案2】:

    这两种过滤方式在大多数情况下是等价的,但是当基于 ForeignKey 或 ManyToManyField 查询对象时,它们会略有不同。

    来自the documentation 的示例。

    型号
    博客与条目是一对多的关系。

    from django.db import models
    
    class Blog(models.Model):
        ...
    
    class Entry(models.Model):
        blog = models.ForeignKey(Blog)
        headline = models.CharField(max_length=255)
        pub_date = models.DateField()
        ...
    

    对象
    假设这里有一些博客和条目对象。

    查询

    Blog.objects.filter(entry__headline_contains='Lennon', 
        entry__pub_date__year=2008)
    Blog.objects.filter(entry__headline_contains='Lennon').filter(
        entry__pub_date__year=2008)  
        
    

    对于第一个查询(单个过滤器之一),它仅匹配 blog1。

    对于第二个查询(链式过滤器之一),它会过滤掉 blog1 和 blog2。
    第一个过滤器将查询集限制为 blog1、blog2 和 blog5;第二个过滤器将博客集进一步限制为 blog1 和 blog2。

    你应该意识到这一点

    我们使用每个过滤器语句过滤博客项目,而不是条目项目。

    所以,不一样,因为 Blog 和 Entry 是多值关系。

    参考:https://docs.djangoproject.com/en/1.8/topics/db/queries/#spanning-multi-valued-relationships
    如有错误,请指正。

    编辑:将 v1.6 更改为 v1.8,因为 1.6 链接不再可用。

    【讨论】:

    • 您似乎在“匹配”和“过滤掉”之间混淆了。如果您坚持“此查询返回”,它会更清晰。
    • 非常好的示意图示例,阐明了两者之间的区别。
    【解决方案3】:

    正如您在生成的 SQL 语句中看到的那样,区别不是某些人可能怀疑的“或”。这就是 WHERE 和 JOIN 的放置方式。

    Example1(同一个连接表):

    (来自https://docs.djangoproject.com/en/dev/topics/db/queries/#spanning-multi-valued-relationships的示例)

    Blog.objects.filter(entry__headline__contains='Lennon', entry__pub_date__year=2008)
    

    这将为您提供具有 一个 条目的所有博客,其中包含 (entry_headline_contains='Lennon') 和 (entry__pub_date__year=2008),这就是您的会从这个查询中得到期望。 结果: 使用 {entry.headline: 'Life of Lennon', entry.pub_date: '2008'}

    预订

    示例 2(链式)

    Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)
    

    这将涵盖示例 1 的所有结果,但会产生更多的结果。因为它首先使用 (entry_headline_contains='Lennon') 过滤所有博客,然后从结果过滤器 (entry__pub_date__year=2008) 中过滤。

    不同之处在于它还会为您提供如下结果: 使用 {entry.headline: 'Lennon', entry.pub_date: 2000}, {entry.headline: 'Bill', entry.pub_date: 2008}

    预订>

    你的情况

    我认为你需要的是这个:

    Book.objects.filter(inventory__user__profile__vacation=False, inventory__user__profile__country='BR')
    

    如果你想使用 OR,请阅读:https://docs.djangoproject.com/en/dev/topics/db/queries/#complex-lookups-with-q-objects

    【讨论】:

    • 第二个例子并不是真的。所有链接的过滤器都应用于查询的对象,即它们在查询中被 ANDed 在一起。
    • 我相信示例2是正确的,实际上是从Django官方文档中提取的解释,如引用。我可能不是最好的解释者,对此我深表歉意。示例 1 是一个直接的 AND,正如您在普通 SQL 编写中所期望的那样。示例 1 给出如下内容:'SELECT blog JOIN entry WHERE entry.head_line LIKE "Lennon" AND entry.year == 2008 示例 2 给出如下内容:'SELECT blog JOIN entry WHERE entry.head_list LIKE "Lennon" UNION SELECT blog JOIN entry WHERE entry.head_list LIKE "Lennon"'
    • 先生,您说的很对。匆忙中我错过了一个事实,即我们的过滤标准指向一对多关系,而不是博客本身。
    【解决方案4】:

    来自Django docs

    为了处理这两种情况,Django 采用一致的方式处理 filter() 调用。单个 filter() 调用中的所有内容同时应用以过滤出符合所有这些要求的项目。连续的 filter() 调用进一步限制了对象的集合,但对于多值关系,它们适用于链接到主模型的任何对象,不一定是之前的 filter() 调用选择的那些对象。

    • 很明显,单个filter() 中的多个条件是同时应用的。 这意味着这样做:
    objs = Mymodel.objects.filter(a=True, b=False)
    

    将返回一个带有来自模型 Mymodel 的原始查询集,其中 a=True AND b=False

    • 在某些情况下,连续的filter() 将提供相同的结果。正在做:
    objs = Mymodel.objects.filter(a=True).filter(b=False)
    

    将返回一个带有来自模型 Mymodel 的原始查询集,其中 a=True AND b=False 也是。由于您“首先”获得了一个带有 a=True 记录的查询集,然后它仅限于同时拥有 b=False 的记录。

    • 链接filter() 的区别在于存在multi-valued relations,这意味着您正在通过其他模型(例如文档中给出的示例,在博客和条目模型之间)。据说那样的话(...) they apply to any object linked to the primary model, not necessarily those objects that were selected by an earlier filter() call.

    这意味着它将后续filter()直接应用于目标模型,而不是之前的filter()

    如果我以文档为例:

    Blog.objects.filter(entry__headline__contains='Lennon').filter(entry__pub_date__year=2008)
    

    请记住,被过滤的是模型Blog,而不是Entry。因此它将独立处理 2 个filter()

    例如,它将返回一个带有博客的查询集,其中包含包含“列侬”的条目(即使它们不是来自 2008 年)和来自 2008 年的条目(即使它们的标题不包含“列侬”)

    THIS ANSWER 在解释中更进一步。和原来的问题类似。

    【讨论】:

      【解决方案5】:

      有时您不想像这样将多个过滤器连接在一起:

      def your_dynamic_query_generator(self, event: Event):
          qs \
          .filter(shiftregistrations__event=event) \
          .filter(shiftregistrations__shifts=False)
      

      下面的代码实际上不会返回正确的东西。

      def your_dynamic_query_generator(self, event: Event):
          return Q(shiftregistrations__event=event) & Q(shiftregistrations__shifts=False)
      

      您现在可以做的是使用注释计数过滤器。

      在这种情况下,我们计算属于某个事件的所有班次。

      qs: EventQuerySet = qs.annotate(
          num_shifts=Count('shiftregistrations__shifts', filter=Q(shiftregistrations__event=event))
      )
      

      之后就可以按注解过滤了。

      def your_dynamic_query_generator(self):
          return Q(num_shifts=0)
      

      这种解决方案在大型查询集上也更便宜。

      希望这会有所帮助。

      【讨论】:

        【解决方案6】:

        在评论中看到这个,我认为这是最简单的解释。

        filter(A, B) 是 AND ; filter(A).filter(B) 是 OR

        【讨论】:

        • @lbris 这曾经是真的吗?
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-06-11
        • 2021-10-23
        • 2012-09-18
        • 2012-03-25
        • 2010-12-22
        相关资源
        最近更新 更多