Python Django:如何使用子查询注释M2M或OneToMany字段?

Python Django:如何使用子查询注释M2M或OneToMany字段?,python,django,django-queryset,Python,Django,Django Queryset,我有Order对象和OrderOperation对象,它们表示订单上的操作(创建、修改、取消) 从概念上讲,一个订单有1到多个订单操作。每次对订单进行操作时,都会在此操作中计算总数。这意味着,当我需要查找订单的属性时,我只需要使用子查询获取最后一个订单操作属性 简化代码 这样,我就可以运行my\u order\u total=order.objects.annotated\u total()[0].oo\u total。它工作得很好 问题 计算总数很容易,因为它是一个简单的值。但是,当存在M2M

我有
Order
对象和
OrderOperation
对象,它们表示订单上的操作(创建、修改、取消)

从概念上讲,一个订单有1到多个订单操作。每次对订单进行操作时,都会在此操作中计算总数。这意味着,当我需要查找订单的属性时,我只需要使用子查询获取最后一个订单操作属性

简化代码 这样,我就可以运行
my\u order\u total=order.objects.annotated\u total()[0].oo\u total
。它工作得很好

问题 计算总数很容易,因为它是一个简单的值。但是,当存在M2M或OneToMany字段时,此方法不起作用。例如,使用上面的示例,我们添加以下字段:

class OrderOperation(models.Model):
    order = models.ForeignKey(Order)
    total = DecimalField(max_digits=9, decimal_places=2)
    ordered_articles = models.ManyToManyField(Article,through='orders.OrderedArticle')                                       
编写以下内容不起作用,因为它只返回1个外键(不是所有FK的列表):

目的 整个目的是允许用户在所有订单中搜索,在输入中提供列表或文章。例如:“请查找至少包含第42条或第43条的所有订单”,或“请查找恰好包含第42条和第43条的所有订单”,等等

如果我能得到这样的东西:

>>> Order.objects.annotated_ordered_articles()[0].oo_ordered_articles
<ArticleQuerySet [<Article: Article42>, <Article: Article43>]>
那会解决我的问题

我当前的想法
  • 也许像(我正在使用pgSQL)这样的东西可以做到这一点,但我不知道如何在我的案例中使用它
  • 这可能与
    values()
    方法有关,该方法似乎不打算处理文档中所述的M2M和1TM关系:
values()和values_list()都是用于优化 特定用例:检索数据子集而不需要 创建模型实例。在处理问题时,这个比喻就不成立了 多对多和其他多值关系(如一对多 反向外键的关系),因为“一行一个对象” 假设不成立


令我惊讶的是,你对
ArrayAg
的想法是正确的。我不知道有一种方法可以用数组进行注释(我相信除了Postgres之外,后端仍然没有这种方法)

然后,您可以使用以下命令筛选结果查询集:

只有当操作可能包含重复的项目时,才需要使用“独特的”

您可能需要调整传递给
ArrayAgg
函数的字段的确切名称。为了使后续过滤工作正常,您可能还需要将
ArrayAgg
中的id字段强制转换为
int
,否则Django会将id数组强制转换为
::serial[]
,我的Postgres抱怨
类型“serial[]”不存在

from django.db.models import IntegerField
from django.contrib.postgres.fields.array import ArrayField
from django.db.models.functions import Cast

ArrayAgg(Cast('order_operation__ordered_articles__id', IntegerField()))
# OR
Cast(ArrayAgg('order_operation__ordered_articles__id'), ArrayField(IntegerField()))

仔细查看您发布的代码,您还必须筛选您感兴趣的
OrderOperation
;上面的查询查看了相关订单的所有操作。

ArrayAgg
如果您只想从所有文章中获取一个变量(即名称),这将非常有用。如果您需要更多,有更好的选择:

prefetch\u相关
相反,您可以将每个
订单
、最新的
订单操作
作为一个整体对象进行预取。这增加了从
OrderOperation
轻松获取任何字段的能力,而无需额外的魔法

唯一需要注意的是,当所选订单没有操作时,您将始终得到一个包含一个操作的列表或一个空列表

要做到这一点,您应该将queryset模型与用于
OrderOperation
的自定义查询结合使用。例如:

from django.db.models import Max, F, Prefetch

last_order_operation_qs = OrderOperation.objects.annotate(
    lop_pk=Max('order__orderoperation__pk')
).filter(pk=F('lop_pk'))

orders = Order.objects.prefetch_related(
    Prefetch('orderoperation_set', queryset=last_order_operation_qs, to_attr='last_operation')
)

然后您可以使用
order.last_操作[0]。ordered_articles
获取特定订单的所有已订购物品。您可以将
预回迁相关('ordered_articles')
添加到第一个queryset,以提高性能并减少对数据库的查询。

您希望的行为是什么?@GwynBleidD问题更新为“目的”部分。是否仅从上一个
OrderOperation
中获取它们?@GwynBleidD是。行为应类似于
total
。除了
total
OrderOperation
中返回一个简单的十进制值,而
ordered\u articles
是多对多字段的管理器,因此处理起来更复杂:)请注意,这实际上是我对上一个问题的改进,但是它不允许对任何使用
预取的内容进行过滤。再次非常感谢,非常有用。。由于我的目的是获取订单,给定一个文章列表作为输入,您认为用这个解决方案替换答案的“预取部分”怎么样:
order\u pks=last\u order\u operation\u qs.filter(ordered\u articles\u in=[1,4])。选择与之相关的('order')。值('order\u pk')
order.objects.filter(pk\u in=order\u pks)
?谢谢。这是最实用和最直观的解决方案。完美!我使用
ArrayAgg(field\u name,distinct=True)
使用最新的Django获得了不同的值。医生。
>>> Order.objects.annotated_ordered_articles()[0].oo_ordered_articles
[42,43]
from django.contrib.postgres.aggregates.general import ArrayAgg

qs = Order.objects.annotate(oo_articles=ArrayAgg(
            'order_operation__ordered_articles__id',
            'DISTINCT'))
# Articles that contain the specified array
qs.filter(oo_articles__contains=[42,43])
# Articles that are identical to the specified array
qs.filter(oo_articles=[42,43,44])
# Articles that are contained in the specified array
qs.filter(oo_articles__contained_by=[41,42,43,44,45])
# Articles that have at least one element in common
# with the specified array
qs.filter(oo_articles__overlap=[41,42])
from django.db.models import IntegerField
from django.contrib.postgres.fields.array import ArrayField
from django.db.models.functions import Cast

ArrayAgg(Cast('order_operation__ordered_articles__id', IntegerField()))
# OR
Cast(ArrayAgg('order_operation__ordered_articles__id'), ArrayField(IntegerField()))
from django.db.models import Max, F, Prefetch

last_order_operation_qs = OrderOperation.objects.annotate(
    lop_pk=Max('order__orderoperation__pk')
).filter(pk=F('lop_pk'))

orders = Order.objects.prefetch_related(
    Prefetch('orderoperation_set', queryset=last_order_operation_qs, to_attr='last_operation')
)