Как отфильтровать объекты для подсчета комментариев в Django?
Рассмотрим простые модели Django Event
а также Participant
:
class Event(models.Model):
title = models.CharField(max_length=100)
class Participant(models.Model):
event = models.ForeignKey(Event, db_index=True)
is_paid = models.BooleanField(default=False, db_index=True)
Легко аннотировать запрос событий с общим количеством участников:
events = Event.objects.all().annotate(participants=models.Count('participant'))
Как комментировать количество участников, отфильтрованных по is_paid=True
?
Мне нужно запрашивать все события независимо от количества участников, например, мне не нужно фильтровать по аннотированному результату. Если есть 0
участники, это нормально, мне просто нужно 0
в аннотированной стоимости.
Пример из документации здесь не работает, потому что он исключает объекты из запроса вместо того, чтобы пометить их 0
,
Обновить. В Django 1.8 появилась новая функция условных выражений, так что теперь мы можем сделать так:
events = Event.objects.all().annotate(paid_participants=models.Sum(
models.Case(
models.When(participant__is_paid=True, then=1),
default=0,
output_field=models.IntegerField()
)))
Обновление 2. В Django 2.0 появилась новая функция условного агрегирования, см. Принятый ответ ниже.
6 ответов
Условная агрегация в Django 2.0 позволяет еще больше уменьшить количество ошибок, которые были в прошлом. Это также будет использовать Postgres ' filter
логика, которая несколько быстрее, чем суммирующий случай (я видел числа около 20-30%, сгруппированные вокруг).
В любом случае, в вашем случае мы рассматриваем что-то простое:
events = Event.objects.annotate(
paid_participants=Count('participants', filter=Q(participants__is_paid=True))
)
В документации есть отдельный раздел о фильтрации аннотаций. Это то же самое, что и условная агрегация, но больше похоже на мой пример выше. В любом случае, это намного здоровее, чем грубые подзапросы, которые я делал раньше.
Только что обнаружил, что в Django 1.8 появилась новая функция условных выражений, так что теперь мы можем сделать так:
events = Event.objects.all().annotate(paid_participants=models.Sum(
models.Case(
models.When(participant__is_paid=True, then=1),
default=0, output_field=models.IntegerField()
)))
ОБНОВИТЬ
Подход подзапроса, который я упоминаю, теперь поддерживается в Django 1.11 через выражения подзапроса.
Event.objects.annotate(
num_paid_participants=Subquery(
Participant.objects.filter(
is_paid=True,
event=OuterRef('pk')
).values('event')
.annotate(cnt=Count('pk'))
.values('cnt'),
output_field=models.IntegerField()
)
)
Я предпочитаю это агрегации (сумма + регистр), потому что она должна быть быстрее и проще для оптимизации (при правильной индексации).
Для более старой версии то же самое может быть достигнуто с помощью .extra
Event.objects.extra(select={'num_paid_participants': "\
SELECT COUNT(*) \
FROM `myapp_participant` \
WHERE `myapp_participant`.`is_paid` = 1 AND \
`myapp_participant`.`event_id` = `myapp_event`.`id`"
})
Я бы предложил использовать .values
метод вашего Participant
вместо этого набор запросов.
Короче говоря, то, что вы хотите сделать, дается:
Participant.objects\
.filter(is_paid=True)\
.values('event')\
.distinct()\
.annotate(models.Count('id'))
Полный пример выглядит следующим образом:
Создать 2
Event
s:event1 = Event.objects.create(title='event1') event2 = Event.objects.create(title='event2')
добавлять
Participant
с ними:part1l = [Participant.objects.create(event=event1, is_paid=((_%2) == 0))\ for _ in range(10)] part2l = [Participant.objects.create(event=event2, is_paid=((_%2) == 0))\ for _ in range(50)]
Сгруппировать все
Participant
с ихevent
поле:Participant.objects.values('event') > <QuerySet [{'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 1}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, {'event': 2}, '...(remaining elements truncated)...']>
Здесь отчетливое необходимо:
Participant.objects.values('event').distinct() > <QuerySet [{'event': 1}, {'event': 2}]>
Какие
.values
а также.distinct
здесь делают то, что они создают два ведраParticipant
сгруппированы по элементамevent
, Обратите внимание, что эти ведра содержатParticipant
,Затем вы можете аннотировать эти сегменты, поскольку они содержат набор оригинальных
Participant
, Здесь мы хотим посчитать количествоParticipant
, это просто делается путем подсчетаid
с элементов в этих ведрах (так как теParticipant
):Participant.objects\ .values('event')\ .distinct()\ .annotate(models.Count('id')) > <QuerySet [{'event': 1, 'id__count': 10}, {'event': 2, 'id__count': 50}]>
Наконец вы хотите только
Participant
сis_paid
являющийсяTrue
Вы можете просто добавить фильтр перед предыдущим выражением, и это даст выражение, показанное выше:Participant.objects\ .filter(is_paid=True)\ .values('event')\ .distinct()\ .annotate(models.Count('id')) > <QuerySet [{'event': 1, 'id__count': 5}, {'event': 2, 'id__count': 25}]>
Единственным недостатком является то, что вы должны получить Event
после того, как у вас есть только id
из метода выше.
Для Django 3.x просто напишите фильтр после аннотации:
User.objects.values('user_id')
.annotate(xyz=models.Sum('likes'))
.filter(xyz__gt=100)
Выше xyz не является полем модели в модели пользователя, и здесь мы фильтруем пользователей, которым нравится (или xyz) более 100.
Какой результат я ищу:
- Люди (исполнители), у которых задачи добавлены в отчет. - Общее количество уникальных людей
- Люди, у которых в отчет добавлены задачи, но только для задач, платежеспособность которых больше 0.
В общем, мне пришлось бы использовать два разных запроса:
Task.objects.filter(billable_efforts__gt=0)
Task.objects.all()
Но мне нужны оба в одном запросе. Отсюда:
Task.objects.values('report__title').annotate(withMoreThanZero=Count('assignee', distinct=True, filter=Q(billable_efforts__gt=0))).annotate(totalUniqueAssignee=Count('assignee', distinct=True))
Результат:
<QuerySet [{'report__title': 'TestReport', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}, {'report__title': 'Utilization_Report_April_2019', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}]>