Django:如何使用 Prefetch 对象“加入”两个查询集?
Posted
技术标签:
【中文标题】Django:如何使用 Prefetch 对象“加入”两个查询集?【英文标题】:Django: How to "join" two querysets using Prefetch Object? 【发布时间】:2021-11-15 23:11:43 【问题描述】:上下文
我对 Django 很陌生,我正在尝试编写一个复杂的查询,我认为在原始 SQL 中很容易写入,但我正在努力使用 ORM。
型号
我有几个名为SignalValue
、SignalCategory
、SignalSubcategory
、SignalType
、SignalSubtype
的模型具有与以下模型相同的结构:
class MyModel(models.Model):
id = models.BigAutoField(primary_key=True)
name = models.CharField()
fullname = models.CharField()
我还有表示模型 SignalValue
与其他模型 SignalCategory
、SignalSubcategory
、SignalType
、SignalSubtype
之间关系的显式模型。这些关系中的每一个分别命名为SignalValueCategory
、SignalValueSubcategory
、SignalValueType
、SignalValueSubtype
。下面以SignalValueCategory
模型为例:
class SignalValueCategory(models.Model):
signal_value = models.OneToOneField(SignalValue)
signal_category = models.ForeignKey(SignalCategory)
最后,我还有以下两个模型。 ResultSignal
存储了与模型相关的所有信号Result
:
class Result(models.Model):
pass
class ResultSignal(models.Model):
id = models.BigAutoField(primary_key=True)
result = models.ForeignKey(
Result
)
signal_value = models.ForeignKey(
SignalValue
)
查询
我想要达到的目标如下。
对于给定的Result
,我想检索属于它的所有ResultSignal
,过滤它们以保留我感兴趣的内容,并用两个字段注释它们,我们将称之为filter_group_id
和filter_group_name
。两个字段的值由给定的ResultSignal
的SignalValue
确定。
在我看来,实现这一点的最简单方法是首先用它们对应的filter_group_name
和filter_group_id
注释SignalValue
s,然后将生成的QuerySet
与ResultSignal
s 连接起来。但是,我认为在 Django 中不可能将两个QuerySet
s 连接在一起。因此,我认为我们可以使用Prefetch
对象来实现我想要做的事情,但似乎我无法使其正常工作。
代码
我现在将描述我的查询的当前状态。
首先,用它们对应的filter_group_name
和filter_group_id
注释SignalValue
s。请注意,以下代码中的filter_aggregator
只是一个复杂的过滤器,它只允许我选择想要的SignalValue
s。 group_filter
是同一个过滤器,但作为子过滤器列表。此外,filter_name_case
是一个条件表达式(Case()
构造):
# Attribute a group_filter_id and group_filter_name for each signal
signal_filters = SignalValue.objects.filter(
filter_aggregator
).annotate(
filter_group_id=Window(
expression=DenseRank(),
order_by=group_filters
),
filter_group_name=filter_name_case
)
然后,尝试加入/注释SignalResult
s:
prefetch_object = Prefetch(
lookup="signal_value",
queryset=signal_filters,
to_attr="test"
)
result_signals: QuerySet = (
last_interview_result
.resultsignal_set
.filter(signal_value__in=signal_values_of_interest)
.select_related(
'signal_value__signalvaluecategory__signal_category',
'signal_value__signalvaluesubcategory__signal_subcategory',
'signal_value__signalvaluetype__signal_type',
'signal_value__signalvaluesubtype__signal_subtype',
)
.prefetch_related(
prefetch_object
)
.values(
"signal_value",
"test",
category=F('signal_value__signalvaluecategory__signal_category__name'),
subcategory=F('signal_value__signalvaluesubcategory__signal_subcategory__name'),
type=F('signal_value__signalvaluetype__signal_type__name'),
subtype=F('signal_value__signalvaluesubtype__signal_subtype__name'),
)
)
通常,据我了解,生成的QuerySet
应该有一个现在可用的字段“test”,它将包含signal_filter
的字段,第一个QuerySet
。但是,Django 抱怨在我的代码的最后一部分调用.values(...)
时找不到"test"
:Cannot resolve keyword 'test' into field. Choices are: [...]
。就好像Prefetch
对象的to_attr
参数根本没有考虑进去。
问题
-
我是否误解了
annotate()
和prefetch_related()
函数的功能?如果不是,我在代码中做错了什么,指定参数to_attr
不存在于我的结果QuerySet
中?
有没有更好的方法在 Django 中加入两个 QuerySet
s 或者我最好使用 RawSQL?另一种方法是切换到 Pandas 以在内存中进行连接,但通过精心设计的查询在 SQL 端进行此类转换通常更有效。
【问题讨论】:
【参考方案1】:您走在正确的道路上,但只是缺少预取功能。
您的注释是正确的,但“测试”预取并不是真正的属性。您批量处理SELECT * FROM signal_value
查询,因此您不必执行每行的选择。只需删除“测试”注释就可以了。 https://docs.djangoproject.com/en/3.2/ref/models/querysets/#prefetch-related
请不要使用 pandas,这绝对没有必要,而且开销很大。正如你自己所说,在 sql 端进行转换更有效
【讨论】:
我不太清楚你的意思。你的意思是我应该删除prefetch_object
的参数to_attr
,然后我应该能够像.values([...], signal_value__filter_group_id)
那样访问signal_value
自定义注释字段吗?如果是这种情况,我已经对其进行了测试,但我得到了一个类似的错误,group_filter_id
无法解决。而关于“测试”不是一个属性,它看起来像是文档中的一个属性。所以我必须错过一些东西。请您也澄清一下这部分吗?
我今天确实遇到了一个类似的例子。虽然它没有访问查询集中的预提取。我认为您可以按 result_signal 访问:for result_signal in result_signals: test = next( iter(result_signal.test), None )
【参考方案2】:
来自prefetch_related
上的文档:
请记住,与 QuerySet 一样,任何暗示不同数据库查询的后续链接方法都将忽略以前缓存的结果,并使用新的数据库查询检索数据。
这不是很明显,但 values()
调用是这些链接方法的一部分,它们暗示了不同的查询,实际上会取消 prefetch_related
。如果您删除它,这应该可以工作。
【讨论】:
我试图删除.values()
调用,但之后我仍然无法访问to_attr="test"
值。更准确地说,我在删除呼叫后做了以下操作:result_signals.first().test
。我认为这应该是访问我想要的内容的适当方式吗?对于暗示不同数据库查询的链接方法,.values()
方法应该等同于 SQL 中的简单SELECT
子句。如果我们甚至不能在预取的带注释的 QuerySet 上这样做,那么使用 ORM 有什么意义呢?必须在 Python 中进行选择确实没有意义。
result_signals.first()
也意味着另一个查询,所以在这种情况下你可以做的是result_signals.all()[0].test
,如文档中所述。关于.values()
,它也做了一个group by
。我认为在这种情况下您可以使用.only()
,但我还没有尝试过它是否也会破坏缓存。
所以我刚刚检查过,但它似乎没有更好的工作:(。使用result_signals.all()[0].test
时仍然有同样的错误,即'ResultSignal' object has no attribute 'test'
。
我知道你能分享你拥有的所有最新的相关代码吗?
实际上,我已将帖子中的代码放在一个单独的函数中以避免修改它,直到我在 SO 上找到正确的解决方案(同时作为临时解决方案,我正在使用提到的 Pandas 在不同功能中的替代方案,即使我很清楚发生的开销)。所以从那以后没有代码更新。如果有任何其他信息可以帮助我找到问题的根源,请告诉我!以上是关于Django:如何使用 Prefetch 对象“加入”两个查询集?的主要内容,如果未能解决你的问题,请参考以下文章
迭代 Django 中的相关对象:循环查询集或使用单行 select_related(或 prefetch_related)
如何使用 prefetch_related 获取 Django 相关模型中的最新位置
pythonのdjango select_related 和 prefetch_related()