Django:如何使用SubQuery注释M2M或OneToMany字段?

时间:2019-02-25 09:52:12

标签: python django django-queryset

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

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

简化代码

class OrderOperation(models.Model):
    order = models.ForeignKey(Order)
    total = DecimalField(max_digits=9, decimal_places=2)

class Order(models.Model)
    # ...

class OrderQuerySet(query.Queryset):

    @staticmethod
    def _last_oo(field):
        return Subquery(OrderOperation.objects
                        .filter(order_id=OuterRef("pk"))
                        .order_by('-id')
                        .values(field)
                        [:1])

    def annotated_total(self):
        return self.annotate(oo_total=self._last_oo('total'))

这样,我可以运行my_order_total = Order.objects.annotated_total()[0].oo_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的列表):

def annotated_ordered_articles(self):
    return self.annotate(oo_ordered_articles=self._last_oo('ordered_articles'))

目的

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

如果我能得到类似的东西:

>>> Order.objects.annotated_ordered_articles()[0].oo_ordered_articles
<ArticleQuerySet [<Article: Article42>, <Article: Article43>]>

甚至:

>>> Order.objects.annotated_ordered_articles()[0].oo_ordered_articles
[42,43]

那可以解决我的问题。

我当前的想法

  • 也许像ArrayAgg之类的东西(我正在使用pgSQL)可以解决问题,但是我不确定在我的情况下如何使用它。
  • 也许这与values()方法有关,该方法似乎并不旨在处理文档中所述的M2M和1TM关系:
  

values()和values_list()均旨在优化   特定的用例:检索数据的子集,而不会产生额外的开销   创建模型实例。当处理   多对多和其他多值关系(例如一对多   反向外键的关系),因为“一行,一个对象”   假设不成立。

2 个答案:

答案 0 :(得分:1)

令我惊讶的是,您对ArrayAgg的想法是正确的。我不知道有什么方法可以对数组进行注释(而且我相信,除了Postgres之外,还没有后端)。

from django.contrib.postgres.aggregates.general import ArrayAgg

qs = Order.objects.annotate(oo_articles=ArrayAgg(
            'order_operation__ordered_articles__id',
            'DISTINCT'))

然后您可以使用ArrayField lookups过滤结果查询集:

# 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])

'DISTINCT'仅在操作可能包含重复的文章时才需要。

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

type "serial[]" does not exist

更仔细地查看您发布的代码,您还必须过滤感兴趣的一个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())) ;上面的查询查看了相关订单的所有操作。

答案 1 :(得分:1)

如果您只想从所有文章中获取一个变量(即名称),那么

ArrayAgg会很棒。如果您需要更多,还有一个更好的选择:

prefetch_related

相反,您可以预取每个Order, lates OrderOperation as a whole object. This adds the ability to easily get any field from OrderOperation`,而无需额外的魔法。

唯一需要注意的是,在没有选定订单的任何操作的情况下,您将始终获得仅包含一个操作的列表或空列表。

为此,您应该将prefetch_related查询集模型与Prefetch object一起使用,并为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_operation[0].ordered_articles来获取特定订单的所有订购商品。您可以在第一个查询集中添加prefetch_related('ordered_articles'),以提高性能并减少对数据库的查询。