Аннотирование Django QuerySet с подсчетом подзапроса
Я создаю доску объявлений о работе. Каждая Job может иметь несколько связанных с ней Location объектов.
Я разработал свои модели местоположения и работы следующим образом:
class Location(BaseModel):
slug = models.CharField(unique=True)
city = models.OneToOneField(to="City", null=True)
state = models.ForeignKey(to="State", null=True)
country = models.ForeignKey(to="Country")
class Job(BaseModel):
title = models.CharField()
description = models.TextField()
locations = models.ManyToManyField(
to="Location",
related_name="jobs",
through="JobLocation",
)
Итак, объект Location имеет возможность ссылаться на наше представление о стране (например, United States), штате (например, New York) или городе (например, Manhattan). Я заполняю поле slug модели Location следующим образом:
Если объект Location - страна, я использую название страны.
united-states
Если объект Location является государством, я использую (название государства + название страны).
new-york-united-states
Если объект Location - город, я использую (название города + название штата + название страны).
manhattan-new-york-united-states
Заполнив таким образом поле slug, я могу упростить запрос всех вакансий в определенном месте с помощью поиска endswith
. Например, если есть вакансия Python в Манхэттене, штат Нью-Йорк, и вакансия React в Бруклине, штат Нью-Йорк, я могу получить все вакансии в штате Нью-Йорк следующим образом:
Job.objects.filter(locations__slug__endswith="new-york-united-states").distinct()
Теперь я хочу получить список всех моих объектов Location, и чтобы каждый элемент Location был аннотирован number_of_jobs
. Это number_of_jobs
должно быть количество всех рабочих мест в данном конкретном местоположении.
Например:
- Объект местоположения Манхэттен должен иметь
number_of_jobs=1
(задание Python) - Объект локации Бруклин должен иметь
number_of_jobs=1
(Работа с React) - Объект локации Нью-Йорк должен иметь
number_of_jobs=2
(задание Python + задание React)
Попытка 1:
Самое простое решение, заключающееся в том, чтобы Count
подставить Location.jobs
, не работает. Он показывает 1 задание для Манхэттена и 1 задание для Бруклина, но 0 заданий для Нью-Йорка.
Location.objects.annotate(number_of_jobs=Count("jobs", distinct=True))
# 1 job for Manhattan
# 1 job for Brooklyn
# 0 job for New York
Попытка 2:
subquery = Job.objects.filter(locations__slug__endswith=OuterRef("slug")).distinct().count()
Location.objects.annotate(number_of_jobs=Subquery(subquery))
Это также не работает, потому что count()
выполняет набор запросов немедленно.
Попытка 3:
Основываясь на ответе Ювраджа, мне удалось приблизиться к ответу, но не совсем.
jobs = Job.objects.filter(locations__slug__endswith=OuterRef("slug"))
subquery = jobs.annotate(job_count=Count("id", distinct=True)).values("job_count")[:1]
Location.objects.annotate(number_of_jobs=Subquery(subquery))
Если я запускаю эти выражения, используя debugsqlshell
из django-debug-toolbar
, и копирую SQL, который создает Django, и удаляю предложение GROUP BY, я получаю 100% правильный ответ. Я пока не понимаю, зачем Django добавляет GROUP BY и почему его удаление дает правильный ответ.
Я нахожусь в затруднительном положении с этим набором вопросов. Буду благодарен за любую помощь.
Есть более эффективный способ решения этой задачи. Вы можете обработать агрегацию с помощью Subquery
без прямого вызова .count()
, который, как вы заметили, оценивает набор запросов раньше времени. Вместо этого следует использовать annotate()
и условный Count
для связанных экземпляров Job.
from django.db.models import Count, OuterRef, Subquery, F
# Subquery to count the jobs related to a location slug usingendswithlookup
subquery = Job.objects.filter(
locations__slug__endswith=OuterRef('slug')).values('id')
# Annotating the location objects with the count of jobs
locations_with_job_count = Location.objects.annotate(
number_of_jobs=Subquery(
Job.objects.filter(locations__slug__endswith=OuterRef('slug'))
.annotate(job_count=Count('id'))
.values('job_count')[:1]
))
Прежде всего, спасибо Ювраджу за ваш ответ. Этот ответ основывается на вашем.
Оказывается, Django автоматически добавляет предложение GROUP BY, когда вы используете агрегатную функцию Count
. Я не совсем понимаю, зачем он добавляет предложение GROUP BY и почему его удаление дает правильные результаты в моем случае, поэтому не буду рассуждать. Но вот ответ, также основанный на комментарии Сергея к этому ответу:
class NonAggregateCount(Count):
# Gets rid of Django's automatic GROUP BY clause
contains_aggregate = False
jobs = Job.objects.filter(locations__slug__endswith=OuterRef("slug"))
subquery = jobs.annotate(job_count=NonAggregateCount("id", distinct=True)).values("job_count")
Location.objects.annotate(number_of_jobs=Subquery(subquery))