Подтвердить что ты не робот

Как фильтровать объекты для аннотации count в 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 появилась новая функция условного агрегирования, см. Принятый ответ ниже.

4b9b3361

Ответ 1

Условное агрегирование в Django 2.0 позволяет вам еще больше уменьшить количество ошибок, которые были в прошлом. Это также будет использовать логику filter Postgres, которая работает несколько быстрее, чем суммированный регистр (я видел числа, например, 20-30%).

В любом случае, в вашем случае мы рассматриваем что-то простое:

from django.db.models import Q, Count
events = Event.objects.annotate(
    paid_participants=Count('participants', filter=Q(participants__is_paid=True))
)

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

Ответ 2

Только что обнаружил, что 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()
    )))

Ответ 3

UPDATE

Подпоследовательный подход, о котором я упоминаю, теперь поддерживается в Django 1.11 через subquery-expressions.

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`"
})

Ответ 4

Я бы предложил использовать .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 из метода выше.

Ответ 5

Какой результат я ищу:

  • Люди (уполномоченные), у которых есть задачи, добавленные в отчет. - Общее количество уникальных людей
  • Люди, у которых есть задачи, добавленные в отчет, но, для задачи, чья способность счета больше, чем 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}]>