Обратный поиск внешних отношений, влияющий на вывод кверисета в Django

Я выполняю сложный запрос в Django для базы данных MySql, и результат не соответствует ожидаемому.

Я использую функцию Django TruncMonth() для аннотирования результатов по месяцам, что работает, как и ожидалось. После аннотирования набора запросов по месяцам я добавляю подсчет количества заказов за месяц, что опять же работает как ожидалось.

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

models.py

class Order(models.Model):
    OrderId = models.BigAutoField(primary_key= True)
    OrderDate = models.DateTimeField(default= timezone.now, verbose_name= 'Order Date')
    ...

class OrderItem(models.Model):
    OrderItemId = models.BigAutoField(primary_key= True)
    ArtworkItemId = models.ForeignKey(ArtworkItem, on_delete= models.SET_NULL, null= True, verbose_name= 'Artwork Item')
    OrderId = models.ForeignKey(Order, on_delete= models.CASCADE, verbose_name= 'Order')
    ItemQuantity = models.IntegerField(verbose_name= 'Quantity')

Полный запрос:

Order.objects.annotate(year= ExtractYear('OrderDate'),
                                        month= TruncMonth('OrderDate')).values(
                                        'year', 'month').filter(
                                        year= timezone.now().year, OrderPaid= True).annotate(
                                        orders= Count('OrderId'), revenue= Sum(F('orderitem__ArtworkItemId__ArtworkPrice') * F('orderitem__ItemQuantity')))

# <QuerySet [{'year': 2022, 'month': 'Jul', 'orders': 1, 'revenue': Decimal('180')}, {'year': 2022, 'month': 'Aug', 'orders': 3, 'revenue': Decimal('525')}]>

Вот полный SQL-запрос, сгенерированный Django, для наглядности:

SELECT django_datetime_extract('year', "art_order"."OrderDate", 'UTC', 'UTC') AS "year",
   django_datetime_trunc('month', "art_order"."OrderDate", 'UTC', 'UTC') AS "month",
   COUNT("art_order"."OrderId") AS "orders",
   CAST(SUM(CAST(("art_artworkitem"."ArtworkPrice" * "art_orderitem"."ItemQuantity") AS NUMERIC)) AS NUMERIC) AS "revenue"
  FROM "art_order"
  LEFT OUTER JOIN "art_orderitem"
    ON ("art_order"."OrderId" = "art_orderitem"."OrderId_id")
  LEFT OUTER JOIN "art_artworkitem"
    ON ("art_orderitem"."ArtworkItemId_id" = "art_artworkitem"."ArtworkItemId")
 WHERE ("art_order"."OrderPaid" AND "art_order"."OrderDate" BETWEEN '''2022-01-01 00:00:00''' AND '''2022-12-31 23:59:59.999999''')
 GROUP BY django_datetime_extract('year', "art_order"."OrderDate", 'UTC', 'UTC'),
   django_datetime_trunc('month', "art_order"."OrderDate", 'UTC', 'UTC')

Однако когда я удаляю аргумент revenue:

Order.objects.annotate(year= ExtractYear('OrderDate'),
                                        month= TruncMonth('OrderDate')).values(
                                        'year', 'month').filter(
                                        year= timezone.now().year, OrderPaid= True).annotate(
                                        orders= Count('OrderId'))

# <QuerySet [{'year': 2022, 'month': 'Jul', 'orders': 1}, {'year': 2022, 'month': 'Aug', 'orders': 2}]>

пара ключ-значение orders имеет правильные значения: 1 для июля и 2 для августа.

Похоже, что поскольку внешний ключ был просмотрен, он подсчитывает количество OrderItems, а не Orders. Я не уверен, что здесь происходит, и буду благодарен за совет.

Вы можете просто сделать следующее:

qs_order = Order.objects.filter(
   OrderDate__year=timezone.now().year
   OrderPaid=True
).annotate(
   revenue=F('orderitem__ArtworkItemId__ArtworkPrice') * F('orderitem__ItemQuantity')
)

count = qs_order.count()
total_revenue = qs_order.aggregate(tot=Sum(F('revenue'))['tot']

Я оставлю свой ответ здесь для всех желающих в будущем, если он окажется полезным.

Мне удалось решить эту проблему, установив distinct = True на Count функцию.

Мое объяснение, полученное в результате тестирования:

Похоже, что при вызове annotate() django затем форматирует SQL с точки зрения обратного отношения (если таковое существует). Так, например, в данном случае все вызовы полей были оценены с позиции обратного отношения, orderitem (несмотря на то, что исходный вызов исходил из Order).

Чтобы было еще понятнее, каждый orderitem, по которому выполнялся цикл, обращался к атрибуту OrderId__OrderId, который каждый раз добавлял 1. Это вместо того, чтобы подойти к этому с другой стороны и посчитать один OrderId для двух orderitems. Таким образом, если в одном заказе два товара, то изначально это воспринимается как два заказа. Установка distinct = True не добавляет дублирующий OrderId к подсчету.

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