Нужен обходной путь для фильтрации по связанной модели и агрегированным полям в Django - PullRequest
0 голосов
/ 03 мая 2010

Я открыл тикет для этой проблемы.

В двух словах вот моя модель:

class Plan(models.Model):
 cap = models.IntegerField()

class Phone(models.Model):
 plan = models.ForeignKey(Plan, related_name='phones')

class Call(models.Model):
 phone = models.ForeignKey(Phone, related_name='calls')
 cost = models.IntegerField()

Я хочу выполнить запрос, подобный этому:

Phone.objects.annotate(total_cost=Sum('calls__cost')).filter(total_cost__gte=0.5*F('plan__cap'))

К сожалению, Django генерирует неверный SQL:

SELECT "app_phone"."id", "app_phone"."plan_id",
SUM("app_call"."cost") AS "total_cost"
FROM "app_phone"
INNER JOIN "app_plan" ON ("app_phone"."plan_id" = "app_plan"."id")
LEFT OUTER JOIN "app_call" ON ("app_phone"."id" = "app_call"."phone_id")
GROUP BY "app_phone"."id", "app_phone"."plan_id"
HAVING SUM("app_call"."cost") >=  0.5 * "app_plan"."cap"

и ошибки с:

ProgrammingError: column "app_plan.cap" must appear in the GROUP BY clause or be used in an aggregate function
LINE 1: ...."plan_id" HAVING SUM("app_call"."cost") >=  0.5 * "app_plan"....

Есть ли обходной путь помимо запуска необработанного SQL?

1 Ответ

1 голос
/ 04 мая 2010

При агрегировании SQL требует, чтобы любое значение в поле было либо уникальным в группе, либо чтобы поле было включено в функцию агрегирования, которая гарантирует, что для каждой группы будет получено только одно значение. Проблема здесь в том, что «app_plan.cap» может иметь много разных значений для каждой комбинации «app_phone.id» и «app_phone.plan_id», поэтому вам нужно указать БД, как их обрабатывать.

Итак, допустимый SQL для вашего результата - одна из двух разных возможностей, в зависимости от того, какой результат вы хотите. Во-первых, вы можете включить app_plan.cap в функцию GROUP BY, чтобы любая отдельная комбинация (app_phone.id, app_phone.plan_id, app_plan.cap) была другой группой:

SELECT "app_phone"."id", "app_phone"."plan_id", "app_plan"."cap",
SUM("app_call"."cost") AS "total_cost"
FROM "app_phone"
INNER JOIN "app_plan" ON ("app_phone"."plan_id" = "app_plan"."id")
LEFT OUTER JOIN "app_call" ON ("app_phone"."id" = "app_call"."phone_id")
GROUP BY "app_phone"."id", "app_phone"."plan_id", "app_plan"."cap"
HAVING SUM("app_call"."cost") >=  0.5 * "app_plan"."cap"

Хитрость заключается в том, чтобы получить дополнительное значение в вызове "GROUP BY". Мы можем облегчить это путем злоупотребления «extra», хотя это жестко закодирует имя таблицы для «app_plan», которая неидеальна - вы можете сделать это программно с классом Plan, если хотите:

Phone.objects.extra({
    "plan_cap": "app_plan.cap"
}).annotate(
    total_cost=Sum('calls__cost')
).filter(total_cost__gte=0.5*F('plan__cap'))

В качестве альтернативы, вы можете заключить app_plan.cap в функцию агрегирования, превратив ее в уникальное значение. Функции агрегирования различаются в зависимости от поставщика БД, но могут включать такие вещи, как AVG, MAX, MIN и т. Д.

SELECT "app_phone"."id", "app_phone"."plan_id",
SUM("app_call"."cost") AS "total_cost",
AVG("app_plan"."cap") AS "avg_cap",
FROM "app_phone"
INNER JOIN "app_plan" ON ("app_phone"."plan_id" = "app_plan"."id")
LEFT OUTER JOIN "app_call" ON ("app_phone"."id" = "app_call"."phone_id")
GROUP BY "app_phone"."id", "app_phone"."plan_id"
HAVING SUM("app_call"."cost") >=  0.5 * AVG("app_plan"."cap")

Вы можете получить этот результат в Django, используя следующее:

Phone.objects.annotate(
    total_cost=Sum('calls__cost'), 
    avg_cap=Avg('plan__cap')
).filter(total_cost__gte=0.5 * F("avg_cap"))

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...