在 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
时,combinator
和 combined_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)
因为注释字段列在真实数据库字段之后,所以 ModelB
的 extra
与 ModelB
的 field1
混合在一起。为确保您获得正确的结果,请确保生成的 SQL 中的字段顺序始终正确 - 带或不带注释。在这种情况下,我建议在 ModelB
上也注释 extra
。
跳转到编辑以查看更多真实代码示例,更改查询顺序后不起作用
这是我的模型:
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
时,combinator
和 combined_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)
因为注释字段列在真实数据库字段之后,所以 ModelB
的 extra
与 ModelB
的 field1
混合在一起。为确保您获得正确的结果,请确保生成的 SQL 中的字段顺序始终正确 - 带或不带注释。在这种情况下,我建议在 ModelB
上也注释 extra
。