在Django中链接多个filter(),这是一个bug吗?

162

我一直认为在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() 调用的查询集在 Inventory 模型上进行了两次连接,有效地创建了两个条件之间的 OR 关系,而第二个查询集将这两个条件 AND 在一起。我原本以为第一次查询也会将这两个条件 AND 在一起。这是 Django 的预期行为还是一个 bug?
相关问题的答案 “在 Django 中使用“.filter().filter().filter()...”是否有缺点? 似乎表明这两个查询集应该是等价的。
6个回答

153
我理解的方式是,它们在设计上略有不同(当然,我乐意接受更正):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)的那些。第二个过滤器选择的条目可能与第一个过滤器中选择的条目相同,也可能不同。

33
虽然有记录,但这种行为似乎违反了最少惊讶原则。当字段在同一模型上时,多个 filter() 会进行 AND 操作,但是当跨越关系时,则会进行 OR 操作。 - gerdemb
5
我相信你在第一段理解有所偏差 - filter(A, B) 是AND的情况(即'docs'中同时包含'lennon'和2008),而filter(A).filter(B)是OR的情况(即'docs'中包含'lennon'或者2008)。当你查看问题中生成的查询时,这种情况就变得很清晰了 - .filter(A).filter(B)的情况会将连接操作执行两次,从而得到一个OR查询。 - Sam
34
filter(A, B) 是对 A 和 B 进行 AND 操作的过滤器。filter(A).filter(B) 则是对 A 和 B 进行 OR 操作的过滤器。 - WeizhongTu
6
"Further restrict" 表示"限制更多",并不表示"限制更少"。 - boh
11
这个回答是不正确的。它不是“OR”。这句话“第二个筛选器进一步将博客集合限制为那些也是(B)。”明确提到“也是(B)”。如果您在这个特定的例子中观察到类似于 OR 的行为,这并不一定意味着您可以概括自己的解释。请查看“Kevin 3112”和“Johnny Tsang”的答案。我认为那些是正确的答案。 - 1man
显示剩余7条评论

110

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

来自文档的示例。

模型
Blog到Entry是一对多的关系。

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项目。

因此,这并不相同,因为Blog和Entry是多值关系。

参考:https://docs.djangoproject.com/en/1.8/topics/db/queries/#spanning-multi-valued-relationships
如果有什么不对的地方,请纠正我。

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


8
你似乎混淆了“匹配”和“过滤掉”的概念。如果你坚持使用“此查询返回”,那么就会更清晰明了。 - OrangeDog
2
非常好的原理图示例,阐明了两者之间的区别。 - muradin

10

来自Django文档

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

  • 明确指出单个filter()中的多个条件同时应用。 这意味着执行以下操作:
objs = Mymodel.objects.filter(a=True, b=False)

将返回一个由模型 Mymodel 中所有满足 a=True b=False 的行组成的QuerySet。

  • 在某些情况下,连续使用 filter() 可能会提供相同的结果。例如:
objs = Mymodel.objects.filter(a=True).filter(b=False)

将返回一个查询集,其中包含模型 Mymodel 中符合条件 a=True b=False的记录。因为您首先获取了具有 a=True 条件的记录集,然后再仅对这些记录进行限制,使其同时满足 b=False 的条件。

  • filter() 链接多值关系时会产生差异,这意味着您要穿越其他模型(例如文档中给出的 Blog 和 Entry 模型之间的关系)。在这种情况下,据说 (...) 它们适用于任何与主模型链接的对象,而不一定是由先前的 filter() 调用选定的那些对象。

这意味着它直接在目标模型上应用连续的 filter(),而不是在先前的 filter() 上进行。

如果我们以文档中的示例为例:

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

记住被过滤的是模型 Blog,而不是 Entry。因此它将独立地处理这两个 filter()
例如,它将返回一个查询集,其中包含具有包含'Lennon'(即使它们不是来自2008年)和来自2008年(即使它们的标题不包含'Lennon')的条目的博客。 此回答更进一步地解释了原理。原问题也很相似。

10

正如您在生成的SQL语句中所看到的,差异并不是像一些人所猜测的那样是"OR"。而是WHERE和JOIN的放置方式不同。

示例1(相同的连接表):

(来自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

在此链接中,您将找到如何使用Q对象进行复杂查询的详细信息。

1
第二个例子实际上并不正确。所有链接的过滤器都应用于查询对象,即它们在查询中被AND在一起。 - Janne
1
我认为示例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"' - Johnny Tsang
1
先生,您说得很对。我匆忙之中错过了一个事实,我们的过滤条件指向的是一对多关系,而不是博客本身。 - Janne

1
有时候您可能不想像这样联合多个过滤器:

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)

这种解决方案在处理大型查询集时也更加经济实惠。
希望这能帮到你。

-3

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

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

只有当每个关联模型都满足两个条件时才为真。


@lbris 这真的永远是真的吗? - Daniel Kaplan
只有当每个链式模型都满足这两个条件时,它才是真的 ;) - DylanYoung

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接