为啥带有 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 返回重复值?的主要内容,如果未能解决你的问题,请参考以下文章

带有 Q 对象的 Django 查询?

带有Q和多个类别的django查询

为啥我不能拥有带有此签名的 Q_PROPERTY?

使用带有外键的 Q 对象定义 django 查询集

使用 F 和 Q 表达式进行 Django 模型过滤

带有注释的Django查询集,为啥将GROUP BY应用于所有字段?