在 Django 中使用 `annotate` + `values` + `union` 结果不正确

Incorrect results with `annotate` + `values` + `union` in Django

跳转到编辑以查看更多真实代码示例,更改查询顺序后不起作用

这是我的模型:

class ModelA(models.Model):
    field_1a = models.CharField(max_length=32)
    field_2a = models.CharField(max_length=32)


class ModelB(models.Model):
    field_1b = models.CharField(max_length=32)
    field_2b = models.CharField(max_length=32)

现在,分别创建 2 个实例:

ModelA.objects.create(field_1a="1a1", field_2a="1a2")
ModelA.objects.create(field_1a="2a1", field_2a="2a2")
ModelB.objects.create(field_1b="1b1", field_2b="1b2")
ModelB.objects.create(field_1b="2b1", field_2b="2b2")

如果我只查询一个带注释的模型,我会得到类似的结果:

>>> ModelA.objects.all().annotate(field1=F("field_1a"), field2=F("field_2a")).values("field1", "field2")
[{"field1": "1a1", "field2": "1a2"}, {"field1": "2a1", "field2": "2a2"}]

这是正确的行为。当我想获得这两个模型的联合时,问题就开始了:

# model A first, with annotate
query = ModelA.objects.all().annotate(field1=F("field_1a"), field2=F("field_2a"))
# now union with model B, also annotated
query = query.union(ModelB.objects.all().annotate(field1=F("field_1b"), field2=F("field_2b")))
# get only field1 and field2
query = query.values("field1", "field2")

# the results are skewed:
assert list(query) == [
    {"field1": 1, "field2": "1a1"},
    {"field1": 1, "field2": "1b1"},
    {"field1": 2, "field2": "2a1"},
    {"field1": 2, "field2": "2b1"},
]

断言正确通过,也就是说结果是错误的。 values() 似乎与变量名不匹配,它只是像元组一样遍历对象。 field1的值其实就是对象的ID,而field2就是field1

这在如此简单的模型中很容易修复,但我的真实模型非常复杂,并且它们具有不同数量的字段。如何正确合并它们?

编辑

您可以在下面找到一个扩展示例,无论 union()values() 的顺序如何,该示例都会失败 - 现在模型稍大一些,不同的字段计数似乎以某种方式混淆了 Django:

# models

class ModelA(models.Model):
    field_1a = models.CharField(max_length=32)
    field_1aa = models.CharField(max_length=32, null=True)
    field_1aaa = models.CharField(max_length=32, null=True)
    field_2a = models.CharField(max_length=32)
    extra_a = models.CharField(max_length=32)


class ModelB(models.Model):
    extra = models.CharField(max_length=32)
    field_1b = models.CharField(max_length=32)
    field_2b = models.CharField(max_length=32)
# test

ModelA.objects.create(field_1a="1a1", field_2a="1a2", extra_a="1extra")
    ModelA.objects.create(field_1a="2a1", field_2a="2a2", extra_a="2extra")
    ModelB.objects.create(field_1b="1b1", field_2b="1b2", extra="3extra")
    ModelB.objects.create(field_1b="2b1", field_2b="2b2", extra="4extra")

    values = ("field1", "field2", "extra")

    query = (
        ModelA.objects.all()
        .annotate(
            field1=F("field_1a"), field2=F("field_2a"), extra=F("extra_a")
        )
        .values(*values)
    )
    query = query.union(
        ModelB.objects.all()
        .annotate(field1=F("field_1b"), field2=F("field_2b"))
        .values(*values)
    )
# outcome

assert list(query) == [
        {"field1": "1a1", "field2": "1a2", "extra": "1extra"},
        {"field1": "2a1", "field2": "2a2", "extra": "2extra"},
        {"field1": "3extra", "field2": "1b1", "extra": "1b2"},
        {"field1": "4extra", "field2": "2b1", "extra": "2b2"},
    ]

我仔细研究了 docs 并且必须承认我没有完全理解为什么你的方法不起作用(根据我的理解它应该)。我认为将 union 应用于具有不同字段名称的查询集似乎会产生奇怪的效果。

无论如何,在合并之前应用值似乎会产生正确的结果:

query = ModelA.objects.all().annotate(field1=F("field_1a"), field2=F("field_2a")).values('field1', 'field2')
query = query.union(ModelB.objects.all().annotate(field1=F("field_1b"), field2=F("field_2b")).values('field1', 'field2'))

这导致了这个查询集

[
    {'field1': '1a1', 'field2': '1a2'}, 
    {'field1': '1b1', 'field2': '1b2'}, 
    {'field1': '2a1', 'field2': '2a2'}, 
    {'field1': '2b1', 'field2': '2b2'}
]

经过一些调试和查看源代码后,我明白了为什么会这样。我要做的是尝试解释为什么 annotate + values 会导致显示 id 以及上述两种情况之间的区别。

为简单起见,我还将编写每个语句的可能结果 sql 查询。

1。 annotate 首先,但在联合查询

上得到 values
qs1 = ModelA.objects.all().annotate(field1=F("field_1a"), field2=F("field_2a"))

当写这样的东西时,django 将获取所有字段 + 带注释的字段,因此生成的 sql 查询如下所示:

select id, field_1a, field_2a, field_1a as field1, field_2a as field2 from ModelA

所以,如果我们有一个 query,它是以下结果:

qs = qs1.union(qs2)

django 的结果 sql 看起来像:

(select id, field_1a, field_2a, field_1a as field1, field_2a as field2 from ModelA)
UNION
(select id, field_1b, field_2b, field_1b as field1, field_2b as field2 from ModelB)

让我们更深入地了解这个 sql 是如何生成的。当我们执行 union 时,combinatorcombined_queries 设置在 qs.query 上,结果 sql 由 combining the sql 个单独查询生成.所以,总结一下:

qs.sql == qs1.sql UNION qs2.sql # in abstract sense

当我们做qs.values('field1', 'field2')时,编译器中的col_count设置为2,也就是字段数。如您所见,returns 5 列上方的联合查询,但在编译器的最终 return 中,结果中的每一行都是 sliced using col_count. Now, this results with only 2 columns is passed back to ValuesIterable where it maps 所选字段中的每个名称以及结果列。这就是它导致错误结果的原因。

2。 annotate + values 对单个查询然后执行 union

现在,让我们看看当annotate直接与values一起使用时会发生什么

qs1 = ModelA.objects.all().annotate(field1=F("field_1a"), field2=F("field_2a")).values('field1', 'field2')

结果 sql 是:

select field_1a as field1, field_2a as field2 from ModelA

现在,当我们合并时:

qs = qs1.union(qs2)

sql 是:

(select field_1a as field1, field_2a as field2 from ModelA)
UNION
(select field_1b as field1, field_2b as field2 from ModelB)

现在,当qs.values('field1', 'field2')执行时,联合查询return的列数有2列,与col_count相同,为2列,每个字段匹配产生预期结果的各个列。


3。不同的字段注释计数和字段排序

在 OP 中,有一种情况即使在 union 之前使用 .values 也不会产生正确的结果。原因是在 ModelB 中没有 extra 字段的注释。

那么,让我们看看为每个模型生成的查询:

ModelA.objects.all()
        .annotate(
            field1=F("field_1a"), field2=F("field_2a"), extra=F("extra_a")
        )
        .values(*values)

SQL变为:

select field_1a as field1, field_2a as field2, extra_a as extra from ModelA

模型B:

ModelB.objects.all()
        .annotate(field1=F("field_1b"), field2=F("field_2b"))
        .values(*values)

SQL:

select extra, field_1b as field1, field_2b as field2 from ModelB

联合是:

(select field_1a as field1, field_2a as field2, extra_a as extra from ModelA)
UNION
(select extra, field_1b as field1, field_2b as field2 from ModelB)

因为注释字段列在真​​实数据库字段之后,所以 ModelBextraModelBfield1 混合在一起。为确保您获得正确的结果,请确保生成的 SQL 中的字段顺序始终正确 - 带或不带注释。在这种情况下,我建议在 ModelB 上也注释 extra