Как отфильтровать объекты для подсчета комментариев в 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'))

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

  1. Создать 2 Event s:

    event1 = Event.objects.create(title='event1')
    event2 = Event.objects.create(title='event2')
    
  2. добавлять 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)]
    
  3. Сгруппировать все 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,

  4. Затем вы можете аннотировать эти сегменты, поскольку они содержат набор оригинальных Participant, Здесь мы хотим посчитать количество Participant, это просто делается путем подсчета id с элементов в этих ведрах (так как те Participant):

    Participant.objects\
        .values('event')\
        .distinct()\
        .annotate(models.Count('id'))
    > <QuerySet [{'event': 1, 'id__count': 10}, {'event': 2, 'id__count': 50}]>
    
  5. Наконец вы хотите только 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}]>
Другие вопросы по тегам