Django 中的 `annotate` + `values` + `union` 结果不正确

问题描述 投票:0回答:3

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

这是我的模型:

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"},
    ]
python django django-models django-orm
3个回答
4
投票

经过一些调试和查看源代码后,我知道为什么会发生这种情况。我要做的是尝试解释为什么执行

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
时,会在
combinator
上设置
combined_queries
qs.query
,并且通过组合各个查询的 sql 来生成生成的 sql。所以,总结一下:

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

当我们执行

qs.values('field1', 'field2')

时,编译器中的
col_count
被设置为2,即字段的数量。正如您所看到的,上面的联合查询返回 5 列,但在编译器的最终返回中,结果中的每一行都使用 
col_count 进行
 切片。现在,这个只有 2 列的 
results
 被传递回 
ValuesIterable
,在那里它将所选字段中的每个名称与结果列进行 
映射。这就是导致错误结果的原因。

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')

 执行时,从联合查询返回的列数有 2 列,与 
col_count
 相同,为 2,并且每个字段与产生预期结果的各个列相匹配。


3.不同的字段注释计数和字段顺序

在OP中,存在一种情况,即使在

.values

之前使用
union
也不会产生正确的结果。原因是在 
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)

因为带注释的字段列在真实数据库字段之后,所以

extra

ModelB
field1
ModelB
 混合在一起。为了确保获得正确的结果,请确保生成的 SQL 中的字段顺序始终正确 - 带或不带注释。在这种情况下,我建议在 
extra
 上也注释 
ModelB


0
投票
我仔细研究了

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'} ]
    

-1
投票
为了长话短说,您必须合并分离的查询集

(查询集不应该有任何共同点!!).

注意:

DJANGO 中所有注解操作,必须在 union 之前

发生这种情况的原因:当您在 Django 中执行 union 操作时,生成的查询集将具有第一个查询集的注释。为了为每个联合操作注释不同的值,您需要在执行联合之前对各个查询集执行注释。



错误示例:


联合与每次迭代中的注释一起存在。

q = Text.objects.none() for page in range(start_page, end_page): sura_aya = json.loads(page_data[f"{page}"]) sura_aya_next = json.loads(page_data[f"{page + 1}"]) q = q | Text.objects.filter(sura=sura_aya[0]).annotate(page=Value(page))



正确示例:


注释操作后并集完全完成。

querysets_to_union = [] for page in range(start_page, end_page): sura_aya = json.loads(page_data[f"{page}"]) sura_aya_next = json.loads(page_data[f"{page + 1}"]) qs = Text.objects.filter(sura=sura_aya[0], aya__gte=sura_aya[1], aya__lte=sura_aya_next[1]) qs = qs.annotate(page=Value(page)) querysets_to_union.append(qs) for qs in querysets_to_union[1:]: final_qs = final_qs.union(qs)
    
© www.soinside.com 2019 - 2024. All rights reserved.