#python #django #aggregate #django-orm #annotate
#python #django #агрегировать #django-orm #аннотировать
Вопрос:
У меня есть несколько моделей 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.
Как я мог бы это решить?
Комментарии:
1. Order.objects.annotate(orderline_total=Sum(F(‘строка заказа__количество’)*F(‘строка заказа__цена’), output_field=FloatField()))
2. @aedry Ваш комментарий решил мою проблему, это проще, чем я себе представлял. Спасибо!
Ответ №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)))
(Я применил output_field=DecimalField
идею из ответа @Todor для согласованности типов)
Ответ №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
Комментарии:
1. Я попробовал ваш код и его использование для 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 вы использовали? Большое спасибо за вашу помощь!2. Эта ошибка не связана с версией Django. Это либо
data
либоqueryset
связано, вы можете проверить сгенерированный запрос с помощьюprint(queryset.query)
и отладить, почему вы получаете несколько строк для подзапроса, при любом изменении вам не хватает первой,.values('order')
по которой будет группироватьсяorder
?3. Нет, я скопировал код из вашего ответа, у вас получилось? Большое спасибо!
4. Да, я работаю над тестовым проектом с 2 заказами и 3 порядковыми строками, как вы можете видеть из примера использования. Опять же, отладьте свой подзапрос, запустите его вручную, и вы увидите, почему вы получаете несколько строк для подзапроса.
5. Привет, @Todor, большое тебе спасибо за твои последующие действия. Я создаю супер простое приложение с минимальным требуемым кодом, и оно сработало. После небольшого исследования я обнаружил, что использование Postgres — это когда ваш код выдает «KeyError: ‘total'».