Django: Вложенные аннотации Sum() в трех отношениях модели

Я использую Django для создания витрины для обработки заказов и испытываю трудности с одной из аннотаций, которую я пытаюсь написать.

Основные модели данных таковы

class Order(ClusterableModel):
    "various model fields about the status, owner, etc of the order"

class OrderLine(Model):
    order = ParentalKey("Order", related_name="lines")
    product = ForeignKey("Product")
    quantity = PositiveIntegerField(default=1)
    base_price = DecimalField(max_digits=10, decimal_places=2) 

class OrderLineOptionValue(Model):
    order_line = ForeignKey("OrderLine", related_name="option_values")
    option = ForeignKey("ProductOption")
    value = TextField(blank=True, null=True)
    price_adjustment = DecimalField(max_digits=10, decimal_places=2, default=0)

Строка заказа представляет один или несколько конкретных продуктов, покупаемых по определенной базовой цене и в определенном количестве. Эта базовая цена была скопирована из модели продукта, чтобы сохранить цену продукта на момент создания заказа.

Поэтому ордер является просто коллекцией нескольких OrderLines

Сложность возникает в модели OrderLineOptionValue, которая представляет собой модификацию базовой цены на основе выбора, сделанного пользователем, и каждая строка заказа может иметь несколько корректировок, если продукт имеет несколько вариантов. Цвет, размер, вес и т.д. могут иметь переменное влияние на цену.

При запросе модели OrderLine я смог успешно аннотировать каждый результат соответствующим итогом строки (база+сумма(цена_корректировки))*количество, используя следующий запрос:

annotation = {
    "line_total": ExpressionWrapper((F("base_price")+Coalesce(Sum("option_values__price_adjustment", output_field=DecimalField(max_digits=10, decimal_places=2)), Value(0)))*F("quantity"), output_field=DecimalField(max_digits=10, decimal_places=2)),
}
OrderLine.objects.all().annotate(**annotation)

Эта аннотация работает корректно во всех тестах, которые я пробовал. Следует отметить, что OrderLines может не иметь корректировок цены, поэтому Coalesce.

Мои проблемы начинаются при попытке аннотировать каждый заказ с его общим итогом, суммируя все его соответствующие статьи вместе. Моя первая попытка привела к исключению Cannot compute Sum('line_total'): 'line_total' is an aggregate, что, как я могу предположить, является действительно незаконным SQL-запросом, поскольку мои практические знания SQL немного подзабыты.

lineItemSubquery=OrderLine.objects.filter(order=OuterRef('pk')).order_by()
#the same annotation as above
lineItemSubquery=lineItemSubquery.annotate(**annotation).values("order")

Order.objets.all().annotate(annotated_total=Coalesce(Subquery(lineItemSubquery.annotate(sum_total=Sum("line_total")).values('sum_total')), 0.0))

Наткнувшись на этот вопрос, я попытался немного перестроить его, и хотя мне удалось заставить его возвращать число, он делает это неправильно, возвращая только первую строку итогов для каждого заказа.

lineItemSubquery=OrderLine.objects.filter(Q(order=OuterRef("pk"))).annotate(**annotation).values("line_total")
Order.objects.all().annotate(annotated_total=Coalesce(Subquery(lineItemSubquery), 0.0))

При нарезке lineItemSubquery [1:2] аннотация также работает, но затем вычисляется сумма второго элемента строки, игнорируя все остальные элементы строки. Я предполагаю, что это побочный продукт вопроса, на который дана ссылка, и как они запрашивают максимум (первый результат в упорядоченном виде) набора значений вместо суммы всего набора данных.

Мой инстинкт подсказывает, что мне нужно найти способ Sum() подзапроса, или что из-за многоуровневого аспекта мне нужен какой-то дополнительный OuterRef, чтобы соединить отношения между всеми тремя моделями? Я уже близок к концу и всерьез подумываю о том, чтобы просто кэшировать вычисленный итог каждой OrderLine непосредственно в поле модели, чтобы полностью избежать этой проблемы. Ответ, вероятно, очень очевиден, но вы будете моим спасителем, если найдете его!

Я занимался именно такой ситуацией в платформе интернет-магазина. Основы наших моделей таковы (с большим количеством полей, чем эти, конечно):

class Product(models.Model):
    base_price = models.DecimalField(...)
    options = models.ManyToManyField('ProductOption', ...)

class ProductOption(models.Model):
    [price - see below]
    ...

class Order(models.Model):
    cost = models.DecimalField(...)

class OrderItem(models.Model):
    order = models.ForeignKey('Order', ...)
    cost = models.DecimalField(...)

class OrderItemOption(models.Model):
    order_item = models.ForeignKey('OrderItem', null=True, blank=True, on_delete=models.CASCADE)
    product_option = models.ForeignKey('ProductOption', null=True, blank=True, on_delete=models.PROTECT)
    value = models.CharField(max_length=255, default='', blank=True)

Атрибут price для продукта может быть либо полем в модели Product (очень упрощенно), либо вычисляемым свойством, основанным, возможно, на таблице цен или любом другом количестве факторов. На мой взгляд, последний подход предпочтительнее. При использовании последнего подхода вы можете либо использовать декоратор @property на объявлении def price(self): для Product, либо определить функцию: def calculate_price(self):.

В связи с этим и отвечая на ваш вопрос, я переопределяю save() на Order и OrderItem, чтобы вычислить стоимость и затем сохранить ее в поле cost каждой из этих моделей. Конечно, вы также должны инициировать вычисление Order.cost из метода OrderItem.save(), чтобы стоимость Order всегда была актуальной. Расчет затрат может стать очень сложным. Я бы никогда не стал пытаться вычислять стоимость в SQL. Кроме того, я всегда предпочитаю переопределять save(), а не использовать сигнал post_save().

Вам придется быть осторожными при запуске вычислений в одной модели из другой модели, чтобы не столкнуться с ошибкой рекурсии.

Описанный выше код (и последующие итерации) работает в производстве уже шесть лет.

Вернуться на верх