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


103

我一直认为在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,而第二个查询集将这两个条件与在一起。我期望第一个查询也可以同时满足这两个条件。这是预期的行为,还是Django中的错误?

相关问题的答案在Django中使用“ .filter()。filter()。filter()...”有不利之处吗?似乎表明两个查询集应该等效。

Answers:


117

我的理解是,它们在设计上有细微的差别(并且我当然愿意接受更正):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)的博客。第二过滤器选择的条目可能与第一过滤器中的条目相同或不同。


18
这种行为尽管有据可查,但似乎违反了最小惊讶原则。当字段在同一模型上时,多个filter()的AND在一起,而在跨越关系时则或在一起。
gerdemb

3
我相信您在第一段中遇到了错误的方法-filter(A,B)是AND情况(文档中为'lennon'AND 2008),而filter(A).filter(B)是OR情况( 'lennon'OR 2008)。当您查看问题中生成的查询时,这很有意义-.filter(A).filter(B)情况两次创建了联接,结果为OR。
山姆

17
过滤器(A,B)是与过滤器(A).filter(B)是OR
WeizhongTu

3
这样further restrict意味着less restrictive

7
这个答案是不正确的。它不是“或”。这句话“第二个过滤器将博客集限制为也属于(B)的博客。” 明确提到“也(B)”。如果在此特定示例中观察到与OR类似的行为,则不一定意味着您可以概括自己的解释。请查看“ Kevin 3112”和“ Johnny Tsang”的回答。我相信这些是正确的答案。
1man

66

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

文档中的示例。


博客到条目模型是一对多关系。

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。

你应该意识到

我们将使用每个过滤器语句而不是Entry项来过滤Blog项。

因此,这是不一样的,因为Blog和Entry是多值关系。

参考:https : //docs.djangoproject.com/zh-CN/1.8/topics/db/queries/#spanning-multi-valued-relationships
如果出现问题,请纠正我。

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


3
您似乎在“匹配”和“过滤”之间混为一谈。如果您坚持使用“此查询返回”,它将更加清晰。
OrangeDog

7

正如您在生成的SQL语句中看到的那样,差异不是某些人可能怀疑的“ OR”。这就是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)

这将为您提供所有具有一个条目的所有Blog,同时具有(entry_ headline _contains ='Lennon')和(entry__pub_date__year = 2008),这是您从该查询中获得的期望。结果:预订时带有{entry.headline:'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


第二个例子实际上不是真的。所有链接的过滤器都应用于查询的对象,即,它们在查询中与在一起。
Janne 2014年

我相信示例2是正确的,并且它实际上是引用的Django官方文档中的解释。我可能不是最好的解释者,对此我可以原谅。例1是直接AND,正如您在普通SQL编写中所期望的那样。示例1给出了这样的内容:'SELECT blog JOIN entry WHERE entry.head_line LIKE“ Lennon ” AND entry.year == 2008示例2提供了这样的东西:'SELECT blog Join条目WHERE entry.head_list LIKE“ Lennon ” UNION SELECT Blog JOIN entry WHERE entry.head_list喜欢“ Lennon ”'
约翰尼·曾

主席先生,您说得很对。匆匆忙忙地错过了一个事实,那就是我们的过滤条件指向的是一对多关系,而不是博客本身。
Janne 2014年

0

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

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)

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

希望这可以帮助。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.