2015-06-10 13 views
47

Rozważmy prostych modeli Django Event i Participant:Jak filtrować obiekty w celu zliczania komentarzy w Django?

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) 

Łatwo jest opisywanie dalszych wydarzeń z ogólnej liczby uczestników:

events = Event.objects.all().annotate(participants=models.Count('participant')) 

Jak adnotacje z liczbą uczestników filtrowany przez is_paid=True?

Muszę wysłać zapytanie wszystkie zdarzenia niezależnie od liczby uczestników, np. Nie muszę filtrować według adnotowanych wyników. Jeśli są uczestnicy 0, to w porządku, po prostu potrzebuję 0 w adnotowanej wartości.

Tu nie działa example from documentation, ponieważ wyklucza obiekty z kwerendy zamiast ich adnotacji z 0.

Aktualizacja. Django 1.8 posiada nowy conditional expressions feature, więc teraz możemy zrobić tak:

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() 
    ))) 

Aktualizacja 2. Django 2.0 posiada nową funkcję Conditional aggregation zobacz the accepted answer poniżej.

Odpowiedz

6

Conditional aggregation w Django 2.0 pozwala na dalsze zmniejszenie ilości faff, które były w przeszłości. Będzie to również wykorzystywać logikę PostgreSta filter, która jest nieco szybsza niż przypadek sumy (widziałem liczby takie jak ok. 20-30%).

W każdym razie, w przypadku, patrzymy na coś tak prostego jak:

events = Event.objects.annotate(
    paid_participants=Count('participants', filter=Q(participants__is_paid=True)) 
) 

Istnieje osobny rozdział w docs około filtering on annotations. To jest to samo, co warunkowa agregacja, ale bardziej jak mój przykład powyżej. Tak czy inaczej, jest to o wiele zdrowsze niż gnarly podkwarki, które robiłem wcześniej.

+0

To wygląda świetnie! :) – rudyryk

+0

BTW, nie ma takiego przykładu przez link dokumentacji, pokazane jest tylko użycie 'agregatu'. Czy przetestowałeś już takie zapytania? (Nie mam i chcę wierzyć! :) – rudyryk

+2

Mam. Oni pracują. Naprawdę trafiłem na dziwną łatę, w której stare (super-skomplikowane) podzapytanie przestało działać po aktualizacji do Django 2.0 i udało mi się zastąpić ją super-prostą filtrowaną liczbą. Na adnotacjach znajduje się lepszy przykład do doc pracy, więc wciągnę to teraz. – Oli

24

UPDATE

Podejście sub-query której wspominam jest teraz obsługiwana w Django 1.11 poprzez 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() 
    ) 
) 

wolę to ponad agregacji (suma + case), dlatego powinna ona być szybciej i łatwiej być zoptymalizowane (z odpowiednim indeksowania).

Dla starszej wersji, to samo można osiągnąć stosując .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`" 
}) 
+0

Dzięki Todor! Wygląda na to, że znalazłem drogę bez użycia '.extra', ponieważ wolę unikać SQL w Django :) Zaktualizuję pytanie. – rudyryk

+1

Nie ma za co, btw Jestem świadomy tego podejścia, ale do tej pory to nie działające rozwiązanie, dlatego o tym nie wspomniałem. Jednak właśnie znalazłem, że zostało to naprawione w 'Django 1.8.2', więc domyślam się, że jesteś z tą wersją i dlatego to działa dla ciebie. Możesz przeczytać więcej na ten temat [tutaj] (http: // stackoverflow.com/questions/29440374/django-adnotate-and-count-how-to-filter-the-ones-to-include-in-count) i [tutaj] (https://code.djangoproject.com/ticket/24766) – Todor

+0

Dostaję, że to daje Brak, kiedy powinno być 0. Ktoś inny to dostaje? – Splatmistro

70

właśnie odkrył, że Django 1.8 posiada nowy conditional expressions feature, więc teraz możemy zrobić tak:

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() 
    ))) 
+0

Czy jest to odpowiednie rozwiązanie, gdy pasujące elementy są liczne? Powiedzmy, że chcę liczyć zdarzenia kliknięć, które miały miejsce w ostatnim tygodniu. – SverkerSbrg

+0

Dlaczego nie? Dlaczego twoja sprawa jest inna? W powyższym przypadku może wystąpić dowolna liczba opłaconych uczestników wydarzenia. – rudyryk

+0

Myślę, że pytanie @SverkerSbrg pyta, czy jest to nieskuteczne dla dużych zestawów, a nie, czy to działa .... prawda? Najważniejsze, aby wiedzieć, że nie robi się tego w pythonie, ale tworzy klauzulę przypadku SQL - patrz https://github.com/django/django/blob/master/django/db/models/expressions.py#L831 - więc będzie to rozsądnie wydajne, prosty przykład będzie lepszy niż sprzężenie, ale bardziej złożone wersje mogą zawierać podzapytania itd. –

1

Proponuję zamiast tego użyj metody .values zestawu zapytań .

Na krótki, co chcesz zrobić jest dana przez:

Participant.objects\ 
    .filter(is_paid=True)\ 
    .values('event')\ 
    .distinct()\ 
    .annotate(models.Count('id')) 

Kompletny przykład jest następująca:

  1. utworzyć 2 Event S:

    event1 = Event.objects.create(title='event1') 
    event2 = Event.objects.create(title='event2') 
    
  2. Dodaj do nich: 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. Grupa wszystkie Participant s przez ich event polu:

    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)...']> 
    

    Tutaj odrębny wymagające:

    Participant.objects.values('event').distinct() 
    > <QuerySet [{'event': 1}, {'event': 2}]> 
    

    Co .values i .distinct robią tutaj jest to, że są one tworząc dwa wiadra Participant s zgrupowane według ich elementów event. Zauważ, że te wiadra zawierają Participant.

  4. Następnie można opisać te segmenty, ponieważ zawierają one zestaw oryginalnych Participant. Tutaj chcemy policzyć Participant, jest to po prostu zrobić przez liczenie id S o elementach w tych wiadrach (ponieważ są to Participant):

    Participant.objects\ 
        .values('event')\ 
        .distinct()\ 
        .annotate(models.Count('id')) 
    > <QuerySet [{'event': 1, 'id__count': 10}, {'event': 2, 'id__count': 50}]> 
    
  5. Wreszcie chcesz tylko Participant z is_paid będąc True , można po prostu dodać filtr przed poprzedniego słowa, a to w wyniku czego otrzymano przedstawione powyżej wyrażenie:

    Participant.objects\ 
        .filter(is_paid=True)\ 
        .values('event')\ 
        .distinct()\ 
        .annotate(models.Count('id')) 
    > <QuerySet [{'event': 1, 'id__count': 5}, {'event': 2, 'id__count': 25}]> 
    

Jedyną wadą jest to th na końcu musisz odzyskać Event, ponieważ masz tylko id z powyższej metody.

Powiązane problemy