Django反向外键/多对多字段AND查询结果差异原因及合理性咨询
Django反向关联查询中AND写法的结果差异分析
正向与反向查询的AND表现差异
- 正向查询中,以下三种AND查询写法结果完全一致,均返回满足
x=1且y=2的QuerySet:Model.objects.filter(x=1) & Model.objects.filter(y=2) Model.objects.filter(x=1, y=2) Model.objects.filter(Q(x=1) & Q(y=2)) - 但在使用反向外键或多对多字段进行AND查询时,不同写法的结果会出现差异。
示例模型与测试数据
模型定义
class Blog(models.Model): name = models.CharField(max_length=100) tagline = models.TextField() class Author(models.Model): name = models.CharField(max_length=100) country = models.ForeignKey(Country, on_delete=models.CASCADE) class Entry(models.Model): blog = models.ForeignKey(Blog, on_delete=models.CASCADE) headline = models.CharField(max_length=255) body_text = models.TextField() pub_date = models.DateField() authors = models.ManyToManyField(Author)
测试数据
| Headline | Pub Date Year | Authors | Blog |
|---|---|---|---|
| Pop 1 | 2023 | Juan | Pop |
| Pop 2 | 2023 | Emma | Pop |
| Pop 3 | 2010 | Juan, Walmer | Pop |
| Other | 2023 | John | Beatles |
需求:获取同时满足关联Entry标题包含"Pop"且发布年份为2023的作者,预期结果为Juan和Emma。
符合预期的查询方式
以下两种写法均能得到预期结果,且生成的SQL完全一致:
1. Filter多参数写法
Author.objects.filter(entry__headline__contains="Pop", entry__pub_date__year=2023)
2. Q对象组合写法
Author.objects.filter(Q(entry__headline__contains="Pop") & Q(entry__pub_date__year=2023))
对应SQL
SELECT "blog_author"."id", "blog_author"."name", "blog_author"."email", "blog_author"."country_id" FROM "blog_author" INNER JOIN "blog_entry_authors" ON ("blog_author"."id" = "blog_entry_authors"."author_id") INNER JOIN "blog_entry" ON ("blog_entry_authors"."entry_id" = "blog_entry"."id") WHERE ("blog_entry"."headline" LIKE %Pop% ESCAPE '\' AND "blog_entry"."pub_date" BETWEEN 2023-01-01 AND 2023-12-31)
这两种写法的核心逻辑是:在同一次关联Entry表的过程中,同时过滤标题和年份两个条件,即要求作者存在至少一条Entry同时满足两个条件,因此结果无重复。
存在问题的查询方式
使用两个独立QuerySet拼接的写法,结果与预期不符:
Author.objects.filter(entry__headline__contains="Pop") & Author.objects.filter(entry__pub_date__year=2023)
查询结果为Juan、Emma、Juan(出现重复的Juan),而非预期的Juan和Emma。
对应SQL
SELECT "blog_author"."id", "blog_author"."name", "blog_author"."email", "blog_author"."country_id" FROM "blog_author" INNER JOIN "blog_entry_authors" ON ("blog_author"."id" = "blog_entry_authors"."author_id") INNER JOIN "blog_entry" ON ("blog_entry_authors"."entry_id" = "blog_entry"."id") LEFT OUTER JOIN "blog_entry_authors" T4 ON ("blog_author"."id" = T4."author_id") LEFT OUTER JOIN "blog_entry" T5 ON (T4."entry_id" = T5."id") WHERE ("blog_entry"."headline" LIKE %Pop% ESCAPE '\' AND T5."pub_date" BETWEEN 2023-01-01 AND 2023-12-31)
疑问解答
1. 为何第三种写法结果不同?
前两种写法是将两个过滤条件绑定到同一次Entry关联上,要求单条Entry同时满足两个条件;而第三种写法是两个独立QuerySet的交集:
- 第一个QuerySet关联Entry筛选出所有有标题含"Pop"文章的作者(包含Juan、Emma、Walmer)
- 第二个QuerySet关联Entry筛选出所有有2023年发布文章的作者(包含Juan、Emma、John)
- Django合并这两个QuerySet时,会保留各自的关联逻辑,导致SQL中对Entry表做了两次独立的JOIN。Juan因为同时有符合第一个条件的Entry(Pop3)和符合第二个条件的Entry(Pop1),两次JOIN后会生成多条记录,最终返回重复的Juan。
2. 该重复结果是否属于预期结果?
不属于预期结果。我们需求是获取满足条件的作者集合,而非关联Entry的记录集合。若要让第三种写法得到正确结果,需手动添加.distinct()方法去重:
(Author.objects.filter(entry__headline__contains="Pop") & Author.objects.filter(entry__pub_date__year=2023)).distinct()
内容的提问来源于stack exchange,提问作者Guzman Ojero
相关产品推荐
相关产品推荐

