Django:如何使用 Prefetch 对象“加入”两个查询集?

Posted

技术标签:

【中文标题】Django:如何使用 Prefetch 对象“加入”两个查询集?【英文标题】:Django: How to "join" two querysets using Prefetch Object? 【发布时间】:2021-11-15 23:11:43 【问题描述】:

上下文

我对 Django 很陌生,我正在尝试编写一个复杂的查询,我认为在原始 SQL 中很容易写入,但我正在努力使用 ORM。

型号

我有几个名为SignalValueSignalCategorySignalSubcategorySignalTypeSignalSubtype 的模型具有与以下模型相同的结构:

class MyModel(models.Model):
    id = models.BigAutoField(primary_key=True)
    name = models.CharField()
    fullname = models.CharField()

我还有表示模型 SignalValue 与其他模型 SignalCategorySignalSubcategorySignalTypeSignalSubtype 之间关系的显式模型。这些关系中的每一个分别命名为SignalValueCategorySignalValueSubcategorySignalValueTypeSignalValueSubtype。下面以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_idfilter_group_name。两个字段的值由给定的ResultSignalSignalValue 确定。

在我看来,实现这一点的最简单方法是首先用它们对应的filter_group_namefilter_group_id 注释SignalValues,然后将生成的QuerySetResultSignals 连接起来。但是,我认为在 Django 中不可能将两个QuerySets 连接在一起。因此,我认为我们可以使用Prefetch 对象来实现我想要做的事情,但似乎我无法使其正常工作。

代码

我现在将描述我的查询的当前状态。

首先,用它们对应的filter_group_namefilter_group_id 注释SignalValues。请注意,以下代码中的filter_aggregator 只是一个复杂的过滤器,它只允许我选择想要的SignalValues。 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
)

然后,尝试加入/注释SignalResults:

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 中加入两个 QuerySets 或者我最好使用 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 相关模型中的最新位置

Django prefetch_related 与限制

pythonのdjango select_related 和 prefetch_related()

如何通过Django中的prefetch_related过滤具有多个条件的反向外键

django- 在另一个 prefetch_related 中使用 prefetch_related