Django ORM множественный эквивалент цепочки JOIN и агрегация

Для следующих моделей Django (лоты показаны просто как пример, могут быть более или менее вложенными):

class ModelA(models.Model):
    value = models.IntegerField()

class ModelB(models.Model):
    modelA = models.ForeignKey(ModelA, on_delete=models.CASCADE)
    value = models.IntegerField()

class ModelC(models.Model):
    modelB = models.ForeignKey(ModelB, on_delete=models.CASCADE)
    value = models.IntegerField()

class ModelD(models.Model):
    modelC = models.ForeignKey(ModelC, on_delete=models.CASCADE)
    value = models.IntegerField()

class ModelE(models.Model):
    modelD = models.ForeignKey(ModelD, on_delete=models.CASCADE)
    value = models.IntegerField()

# etc...

Как мы можем использовать Django ORM для выполнения следующих операций:

например, все моделиЕ для данной моделиА, SQL эквивалент:

SELECT ModelE.*
FROM ModelA
JOIN ModelB ON ModelB.modelA = ModelA.id
JOIN ModelC ON ModelC.modelB = ModelB.id
JOIN ModelD ON ModelD.modelC = ModelC.id
JOIN ModelE ON ModelE.modelD = ModelD.id
WHERE ModelA.id = 1

например, сгруппировать все записи по некоторой модели, SQL-эквивалент:

SELECT ModelC.*, SUM(ModelE.value)
FROM ModelA
JOIN ModelB ON ModelB.modelA = ModelA.id
JOIN ModelC ON ModelC.modelB = ModelB.id
JOIN ModelD ON ModelD.modelC = ModelC.id
JOIN ModelE ON ModelE.modelD = ModelD.id
WHERE ModelA.id = 1
GROUP BY ModelC.id

Конкретный запрос, который я пытаюсь получить, эквивалентен следующему:

SELECT ModelC.value * SUM(ModelE.value)
FROM ModelA
JOIN ModelB ON ModelB.modelA = ModelA.id
JOIN ModelC ON ModelC.modelB = ModelB.id
JOIN ModelD ON ModelD.modelC = ModelC.id
WHERE ModelA.id = 1 AND ModelD.value >= 1 AND ModelD.value < 3
GROUP BY ModelC.id

Мне приходится использовать обходной путь на Python, который довольно неэффективен, но гораздо более понятен. Я надеялся, что есть способ сделать это с помощью Django ORM.

Не уверен, что это соответствует тому, что вы хотите.

Однако, возможно, вы сможете использовать этот код ORM, изменив его.

from django.db.models import F, Sum

queryset = (
    ModelC.objects
          .annotate(
               sum_e_values=Sum('modeld__modele__value'),
               result_value=F('value') * F('sum_e_values'),
           )
          .filter(
               modelB__modelA_id=1,
               modeld__value__gte=1,
               modeld__value__lt=3,
          )
          .values('result_value')
)
print(queryset.query)

Выход:

SELECT ("myapp_modelc"."value" * SUM("myapp_modele"."value")) AS "result_value"
FROM "myapp_modelc"
LEFT OUTER JOIN "myapp_modeld" ON ("myapp_modelc"."id" = "myapp_modeld"."modelC_id")
LEFT OUTER JOIN "myapp_modele" ON ("myapp_modeld"."id" = "myapp_modele"."modelD_id")
INNER JOIN "myapp_modelb" ON ("myapp_modelc"."modelB_id" = "myapp_modelb"."id")
INNER JOIN "myapp_modeld" T6 ON ("myapp_modelc"."id" = T6."modelC_id")
WHERE ("myapp_modelb"."modelA_id" = 1
       AND T6."value" < 3
       AND T6."value" >= 1)
GROUP BY "myapp_modelc"."id",
         "myapp_modelc"."modelB_id",
         "myapp_modelc"."value"

Ответ оказался на удивление простым, но нигде не упоминается в явном виде. Спасибо @gypark за правильный подход!

Подход не точно соответствует заданному SQL, но дает такие же результаты.

Для первой проблемы работает следующий запрос ORM:

# Selects all ModelE for a given ModelA
ModelE.objects.filter(modelD__modelC__modelB__modelA_id = 1)

Это генерирует следующий эквивалентный SQL (измененный для ясности):

SELECT modele.id, modele.modelD_id, modele.name, modele.value
FROM modele
INNER JOIN modeld ON (modele.modelD_id = modeld.id)
INNER JOIN modelc ON (modeld.modelC_id = modelc.id)
INNER JOIN modelb ON (modelc.modelB_id = modelb.id)
INNER JOIN modela ON (modelb.modelA_id = modela.id)
WHERE modela.id = 1

Для второго:

ModelC.objects.filter(modelB__modelA_id=1).annotate(sumE=Coalesce(Sum('modeld__modele__value'), 0))

Что генерирует следующий SQL-эквивалент:

SELECT modelc.id, modelc.modelB_id, modelc.name, modelc.value, COALESCE(SUM(modele.value), 0) AS sumE
FROM modelc
INNER JOIN modelb ON (modelc.modelB_id = modelb.id)
LEFT OUTER JOIN modeld ON (modelc.id = modeld.modelC_id)
LEFT OUTER JOIN modele ON (modeld.id = modele.modelD_id)
WHERE modelb.modelA_id = 1
GROUP BY modelc.id, modelc.modelB_id, modelc.name, modelc.value

Для третьего:

ModelA.objects.filter(
    id=1,
    modelb__modelc__modeld__value__gte=1,
    modelb__modelc__modeld__value__lt=3,
).aggregate(
    sum=Coalesce(Sum(F('modelb__modelc__value') * F('modelb__modelc__modeld__modele__value')), 0)
)

Что дает следующий SQL:

SELECT COALESCE(SUM((modelc.value * modele.value)), 0) AS sum
FROM modela
INNER JOIN modelb ON (modela.id = modelb.modelA_id)
INNER JOIN modelc ON (modelb.id = modelc.modelB_id)
INNER JOIN modeld ON (modelc.id = modeld.modelC_id)
LEFT OUTER JOIN modele ON (modeld.id = modele.modelD_id)
WHERE (modela.id = 1 AND modeld.value >= 1 AND modeld.value < 3)

Просто примечание, Coalesce (from django.db.models.functions import Coalesce) требуется для устранения проблемы возврата None, когда QuerySet пуст.

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