python - 在 Django 中使用 `annotate` + `values` + `union` 的结果不正确
问题描述
跳转到编辑查看更多真实代码示例,更改查询顺序后不起作用
这是我的模型:
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()
and的顺序如何都会失败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"},
]
解决方案
经过一些调试并浏览源代码后,我知道为什么会发生这种情况。我要做的是尝试解释为什么做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
which 是以下结果:
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 是如何生成的。当我们做 aunion
时, acombinator
和combined_queries
被设置qs.query
并且结果 sql 是通过组合单个查询的 sql 生成的。所以,总结一下:
qs.sql == qs1.sql UNION qs2.sql # in abstract sense
当我们这样做时qs.values('field1', 'field2')
,col_count
in 编译器设置为 2,即字段数。如您所见,上面的联合查询返回 5 列,但在编译器的最终返回中,结果中的每一行都使用 col_count
. 现在,results
只有 2 列的 this 被传递回将所选字段中的每个名称与结果列映射ValuesIterable
的位置。这就是导致错误结果的方式。
2. annotate
+values
对个别查询然后执行union
现在,让我们看看直接annotate
使用 with会发生什么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 列,与 2 列相同,col_count
并且每个字段都与产生预期结果的各个列匹配。
3.不同的字段注释计数和字段排序
在 OP 中,即使使用.values
beforeunion
也不会产生正确的结果。原因是在 中,字段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)
因为带注释的字段列在真正的 db 字段之后,所以extra
of与ofModelB
混合在一起。为确保您获得正确的结果,请确保生成的 SQL 中的字段顺序始终正确 - 有或没有注释。在这种情况下,我建议也进行注释。field1
ModelB
extra
ModelB
推荐阅读
- python - pip install, 如何修复 ImportError
- c# - 在 C# 源代码中将 DLL 放在哪里?
- python - 在测试模式下无法单击添加到购物车按钮
- google-chrome-devtools - chrome devtools 的问题显示组件边框
- c++ - 将“VkImage”清除为单一颜色的最佳方法是什么?
- python - 使用 Apahce-beam 在 Python 中删除字典中的第一项
- scala - 在 Scala Spark 中使用全局变量
- c - strtod128 函数和内存泄漏 (gcc 8.3.1)
- laravel - 使用 Laravel envoy 部署时 app.js 无法获取环境变量
- javascript - react.js 状态函数仅在从按钮调用而不是从函数调用时对 statechange 做出反应