如何注释 Django QuerySet 聚合带注释的子查询
How to annotate a Django QuerySet aggregating annotated Subquery
我有几个 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.
我该如何解决这个问题?
您不能使用 .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
正如@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
想法以确保类型一致性)
我有几个 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.
我该如何解决这个问题?
您不能使用 .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
正如@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
想法以确保类型一致性)