为啥带有 Q() 表达式的 Django QuerySet 返回重复值?
Posted
技术标签:
【中文标题】为啥带有 Q() 表达式的 Django QuerySet 返回重复值?【英文标题】:Why is Django QuerySet with Q() expression returning duplicate values?为什么带有 Q() 表达式的 Django QuerySet 返回重复值? 【发布时间】:2016-03-31 19:49:43 【问题描述】:我有以下 2 个 Django 模型:
from mptt.models import MPTTModel, TreeForeignKey
from django.db import models
from django.db.models import Q
class Model1(MPTTModel):
random_field = models.IntegerField()
parent = TreeForeignKey('self', null=True, blank=True)
class Model2(models.Model):
model_1 = models.ManyToManyField(Model1)
@staticmethod
def descendants_queryset(model1):
q = Q()
for curr_descendant in model1.get_descendants:
q |= Q(model_1=curr_descendant)
return q
我已经创建了这样的实例:
>>> a = Model2.objects.create()
>>> b = Model1.objects.create(random_field=1, parent=None)
>>> c = Model1.objects.create(random_field=2, parent=b)
>>> d = Model1.objects.create(random_field=3, parent=b)
>>> a.model_1.add(c)
>>> a.pk
3
当我做一个普通的查询集过滤器时,当我使用 Q() 表达式时,它会产生相同的结果(如预期的那样):
>>> [i.pk for i in Model2.objects.filter(pk=3)]
[3]
>>> [i.pk for i in Model2.objects.filter(Model2.descendants_queryset(b), pk=3)]
[3]
但是当我将 Model1 的另一个实例添加到 ManyToMany 关系时,只有在使用 Q() 表达式进行过滤时才会看到奇怪的重复:
>>> a.model_1.add(d)
>>> [i.pk for i in Model2.objects.filter(pk=3)]
[3]
>>> [i.pk for i in Model2.objects.filter(Model2.descendants_queryset(b), pk=3)]
[3, 3]
我很困惑为什么会发生这种重复。这对我来说似乎是一个错误。我显然可以通过在查询集中添加.distinct()
来解决它。但这似乎没有必要。为什么会发生这种情况?正确的解决方案是什么?
【问题讨论】:
【参考方案1】:我注意到,当您向 a 添加第三个元素时,您的输出不仅重复,而且增加了三倍:
>>> 4 = Model1.objects.create(random_field=3, parent=b)
>>> a.model_1.add(e)
>>> [i.pk for i in Model2.objects.filter(Model2.descendants_queryset(b), pk=3)]
[3, 3, 3]
如果你添加另一个等等,则翻两番......
所以我的猜测是,由于您在 descendants_queryset() 中的 Q()-query 是 ORed,它返回每个对象,其中 b 对象作为父对象,并且过滤器匹配 a 多次(其中有多个对 Model1 对象的引用)。
如果我们查看Model2.objects.filter(Model2.descendants_queryset(b))
的原始 SQL,我们会看到以下内容:
>>> Model2.objects.filter(Model2.descendants_queryset(b)).query.sql_with_params()
(u'SELECT "Foo_model2"."id" FROM "Foo_model2" LEFT OUTER JOIN "Foo_model2_model_1" ON ("Foo_model2"."id" = "Foo_model2_model_1"."model2_id") WHERE ("Foo_model2_model_1"."model1_id" = %s OR "Foo_model2_model_1"."model1_id" = %s OR "Foo_model2_model_1"."model1_id" = %s)', (17, 18, 19))
或更具可读性:
SELECT "Foo_model2"."id"
FROM "Foo_model2"
LEFT OUTER JOIN "Foo_model2_model_1"
ON ("Foo_model2"."id" = "Foo_model2_model_1"."model2_id")
WHERE ("Foo_model2_model_1"."model1_id" = 17
OR "Foo_model2_model_1"."model1_id" = 18
OR "Foo_model2_model_1"."model1_id" = 19)
因此,它实际上将q |= Q(model_1=curr_descendant)
生成的查询与 OR 语句连接起来,该语句返回的不是一个,而是在本例中是三个引用(全部指向同一个 Model2 对象,该对象包含对三个 Model1 对象的 ManyToMany-references) .
这是由于 join 语句造成的 - 有关一些示例,请参阅 here。
如果我们为 pk=3
添加额外的过滤器,它不会进一步限制输出,因为所有返回对象的 PK 都是相同的 (3)。
如果您添加另一个 Model2 对象,并添加 c 作为对新元素 model1 ManyToMany-reference 的引用,您会得到以下结果:
>>> a2 = Model2.objects.create()
>>> a2.model_1.add(c)
>>> [i.pk for i in Model2.objects.filter(Model2.descendants_queryset(b))]
[3, 3, 3, 4]
新的 Model2 对象的 id 也出现在查询集中,因为它也有一个对 model1 对象的引用。
我目前对最佳解决方案没有任何惊人的想法,但在查询集上调用.distinct()
对我来说似乎很简单。
【讨论】:
我也遇到了同样的问题,你找到解决办法了吗? 我有一个导致该问题的多对多字段,如果我删除该字段,则它不会给出任何重复项以上是关于为啥带有 Q() 表达式的 Django QuerySet 返回重复值?的主要内容,如果未能解决你的问题,请参考以下文章