【问题标题】:How to annotate a Django QuerySet aggregating annotated Subquery如何注释 Django QuerySet 聚合带注释的子查询
【发布时间】:2019-08-03 09:16:17
【问题描述】:

我有几个 Django 模型,它们之间存在 FK 关系:

from django.db import models


class Order(models.Model):
    notes = models.TextField(blank=True, null=True)


class OrderLine(models.Model):
    order = models.ForeignKey(Order, on_delete=models.CASCADE)
    quantity = models.PositiveIntegerField()
    price = models.DecimalField(max_digits=8, blank=True, decimal_places=2)

给定OrderLine,您可以按价格计算其总量:

def get_order_line_total(order_line):
    return order_line.quantity * order_line.price

给定Order,您可以将其总数计算为其订单行总数的总和:

def get_order_total(order):
    order_total = 0
    for orderline_for in order.orderline_set.all():
        order_total += (order_line_for.quantity * order_line_for.price)
    return order_total

我想在查询集中对总计进行注释,以便对它们进行过滤、排序等。

对于OrderLine 模型,我发现它非常简单:

from django.db.models import F, FloatField, Sum


annotated_orderline_set = OrderLine.objects.annotate(orderline_total=Sum(F('quantity') * F('price'), output_field=FloatField()))

现在我想在 Order.objects 查询集中注释总数。我想我需要使用子查询,但我不能让它工作。 我的猜测是(不工作):

from django.db.models import F, FloatField, OuterRef, Subquery, Sum


Order.objects.annotate(
    order_total=Subquery(
        OrderLine.objects.filter(
            order=OuterRef('pk')
        ).annotate(
            orderline_total=Sum(F('quantity') * F('price'), output_field=FloatField())
        ).values(
            'orderline_total'
        ).aggregate(
            Sum('orderline_total')
        )['orderline_total__sum']
    )
)

# Not working, returns:
# ValueError: This queryset contains a reference to an outer query and may only be used in a subquery.

我该如何解决这个问题?

【问题讨论】:

  • Order.objects.annotate(orderline_total=Sum(F('orderline__quantity')*F('orderline__price'), output_field=FloatField()))
  • @aedry 您的评论解决了我的问题,它比我想象的要简单。谢谢!

标签: python django aggregate django-orm annotate


【解决方案1】:

正如@aedry 评论所指出的,避免子查询的一个非常简单的解决方案是:

Order.objects.annotate(total=models.Sum(F('orderline_set__quantity') * F('orderline_set__price'), output_field=models.DecimalField(max_digits=10, decimal_places=2)))

(为了类型一致性,我应用了@Todor 答案中的output_field=DecimalField 想法)

【讨论】:

    【解决方案2】:

    您不能使用.aggregate,因为它会立即评估queryset,而您需要将此评估延迟到正在评估外部查询。

    所以正确的做法是.annotate 而不是.aggregate

    class OrderQuerySet(models.QuerySet):
        def annotate_total(self):
            return self.annotate(
                total=models.Subquery(
                    OrderLine.objects.filter(
                        order=models.OuterRef('pk')
                    ).annotate_total()
                    .values('order')
                    .annotate(total_sum=models.Sum('total'))
                    .values('total_sum')
                )
            )
    
    
    class Order(models.Model):
        # ...
        objects = OrderQuerySet.as_manager()
    
    
    class OrderLineQuerySet(models.QuerySet):
        def annotate_total(self):
            return self.annotate(
                total=models.ExpressionWrapper(
                    models.F('quantity')*models.F('price'),
                    output_field=models.DecimalField(max_digits=10, decimal_places=2)
                )
            )
    
    
    class OrderLine(models.Model):
        #...
        objects = OrderLineQuerySet.as_manager()
    
    
    # Usage:
    >>> for l in OrderLine.objects.all().annotate_total():
    ...    print(l.id, l.order_id, l.quantity, l.price, l.total)
    ... 
    1 1 3 20.00 60
    2 1 9 10.00 90
    3 2 18 2.00 36
    
    >>> for o in Order.objects.all().annotate_total():
    ...    print(o.id, o.total)
    ... 
    1 150
    2 36
    

    【讨论】:

    • 我尝试了您的代码和 OrderLine.objects.all().annotate_total() 的用法工作正常,但 Order.objects.all().annotate_total() 用法我得到KeyError: 'total'对于 Django 2.0.7 和 ProgrammingError: more than one row returned by a subquery used as an expression 对于 Django 2.1.7。你让它工作了吗?你用的是什么 Django 版本?非常感谢您的帮助!
    • 这个错误与 Django 的版本无关。它与dataqueryset 相关,您可以使用print(queryset.query) 检查生成的查询并调试为什么您会为子查询获得多行,您是否缺少第一个.values('order') 的任何更改,它将按order 分组?
    • 不,我在你的答案中复制了代码,你让它工作了吗?非常感谢!
    • 是的,我让它在一个有 2 个订单和 3 个 OrderLine 的测试项目上工作,正如您从示例用法中看到的那样。同样,调试您的子查询,手动运行它,您将看到为什么您会为子查询获得多行。
    • 嘿@Todor,非常感谢您的跟进。我用所需的最少代码制作了一个超级简单的应用程序,并且它工作正常。经过一番研究,我发现使用 Postgres 是您的代码抛出“KeyError:'total'”的时候。
    猜你喜欢
    • 1970-01-01
    • 2020-05-23
    • 2017-07-21
    • 1970-01-01
    • 2020-11-26
    • 2015-02-16
    • 2016-11-18
    • 1970-01-01
    • 2023-03-29
    相关资源
    最近更新 更多