Làm cách nào để lọc các đối tượng để đếm số chú thích trong Django?


123

Hãy xem xét các mô hình Django đơn giản EventParticipant:

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)

Thật dễ dàng để chú thích truy vấn sự kiện với tổng số người tham gia:

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

Làm cách nào để chú thích với số lượng người tham gia được lọc theo is_paid=True?

Tôi cần truy vấn tất cả các sự kiện bất kể số lượng người tham gia, ví dụ: tôi không cần lọc theo kết quả có chú thích. Nếu có 0người tham gia, đó là ok, tôi chỉ cần 0trong giá trị chú thích.

Các ví dụ từ tài liệu không làm việc ở đây, bởi vì nó không bao gồm các đối tượng từ truy vấn thay vì chú thích chúng với 0.

Cập nhật. Django 1.8 có tính năng biểu thức điều kiện mới , vì vậy bây giờ chúng ta có thể làm như sau:

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

Cập nhật 2. Django 2.0 có tính năng tổng hợpĐiều kiện mới , hãy xem câu trả lời được chấp nhận bên dưới.

Câu trả lời:


105

Tính năng tổng hợp có điều kiện trong Django 2.0 cho phép bạn giảm thêm lượng lỗi mà điều này đã có trong quá khứ. Điều này cũng sẽ sử dụng Postgres 'filter logic , nhanh hơn một chút so với trường hợp tổng (tôi đã thấy những con số như 20-30% được chia thành xung quanh).

Dù sao, trong trường hợp của bạn, chúng tôi đang xem xét một cái gì đó đơn giản như:

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

Có một phần riêng biệt trong tài liệu về lọc các chú thích . Nó giống như tập hợp có điều kiện nhưng giống như ví dụ của tôi ở trên. Dù theo cách nào thì điều này cũng lành mạnh hơn rất nhiều so với các truy vấn phụ gnarly mà tôi đã làm trước đây.


BTW, không có ví dụ như vậy bằng liên kết tài liệu, chỉ có aggregatecách sử dụng được hiển thị. Bạn đã thử nghiệm các truy vấn như vậy chưa? (Tôi chưa và tôi muốn tin! :)
rudyryk

2
Tôi có. Họ làm việc. Tôi thực sự đã gặp phải một bản vá kỳ lạ trong đó một truy vấn con cũ (siêu phức tạp) ngừng hoạt động sau khi nâng cấp lên Django 2.0 và tôi đã cố gắng thay thế nó bằng một số lượng được lọc siêu đơn giản. Có một ví dụ trong tài liệu tốt hơn cho các chú thích, vì vậy tôi sẽ lấy nó ngay bây giờ.
Oli

1
Có một vài câu trả lời ở đây, đây là cách Django 2.0, và bên dưới bạn sẽ tìm thấy cách Django 1.11 (Truy vấn con) và cách Django 1.8.
Ryan Castner

2
Hãy lưu ý, nếu bạn thử điều này trong Django <2, ví dụ 1.9, nó sẽ chạy mà không có ngoại lệ, nhưng bộ lọc đơn giản là không được áp dụng. Vì vậy, nó có thể hoạt động với Django <2, nhưng không.
djvg

Nếu bạn cần thêm nhiều bộ lọc, bạn có thể thêm chúng vào đối số Q () được phân tách bằng, ví dụ bộ lọc = Q (người tham gia__is_paid = True, somethingelse = value)
Tobit

93

Vừa phát hiện ra rằng Django 1.8 có tính năng biểu thức điều kiện mới , vì vậy bây giờ chúng ta có thể làm như sau:

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

Đây có phải là giải pháp đủ điều kiện khi các mục phù hợp có nhiều không? Giả sử tôi muốn tính các sự kiện nhấp chuột xảy ra vào tuần gần nhất.
SverkerSbrg

Tại sao không? Ý tôi là, tại sao trường hợp của bạn lại khác? Trong trường hợp trên có thể có bất kỳ số lượng người tham gia trả phí nào trong sự kiện.
rudyryk

Tôi nghĩ câu hỏi mà @SverkerSbrg đang hỏi là liệu điều này có kém hiệu quả đối với các bộ lớn hay không, hơn là liệu nó có hoạt động hay không .... đúng không? Điều quan trọng nhất cần biết là nó không hoạt động trong python, nó đang tạo một mệnh đề trường hợp SQL - xem github.com/django/django/blob/master/django/db/models/… - vì vậy nó sẽ hoạt động hợp lý, ví dụ đơn giản sẽ là tốt hơn so với một tham gia, nhưng các phiên bản phức tạp hơn có thể bao gồm các truy vấn con, vv
Hayden Crocker

1
Khi sử dụng điều này với Count(thay vì Sum), tôi đoán chúng ta nên đặt default=None(nếu không sử dụng filterđối số django 2 ).
djvg

41

CẬP NHẬT

Phương pháp truy vấn phụ mà tôi đề cập hiện được hỗ trợ trong Django 1.11 thông qua biểu thức truy vấn con .

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

Tôi thích điều này hơn là tổng hợp (tổng + trường hợp) , vì nó sẽ nhanh hơn và dễ dàng hơn được tối ưu hóa (với lập chỉ mục thích hợp) .

Đối với phiên bản cũ hơn, bạn có thể đạt được điều tương tự bằng cách sử dụng .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`"
})

Cảm ơn Todor! Có vẻ như tôi đã tìm ra cách mà không cần sử dụng .extra, vì tôi muốn tránh SQL trong Django :) Tôi sẽ cập nhật câu hỏi.
rudyryk

1
Bạn được chào đón, btw Tôi biết cách tiếp cận này, nhưng nó là một giải pháp không hoạt động cho đến bây giờ, đó là lý do tại sao tôi không đề cập đến nó. Tuy nhiên, tôi chỉ thấy rằng nó đã được sửa chữa Django 1.8.2, vì vậy tôi đoán bạn đang sử dụng phiên bản đó và đó là lý do tại sao nó hoạt động cho bạn. Bạn có thể đọc thêm về điều đó ở đâyở đây
Todor

2
Tôi hiểu rằng điều này tạo ra Không có khi nó phải là 0. Có ai khác nhận được điều này không?
StefanJCollier

@StefanJCollier Có, tôi Nonecũng vậy. Giải pháp của tôi là sử dụng Coalesce( from django.db.models.functions import Coalesce). Bạn sử dụng nó như thế này: Coalesce(Subquery(...), 0). Tuy nhiên, có thể có một cách tiếp cận tốt hơn.
Adam Taylor

6

Tôi khuyên bạn nên sử dụng .valuesphương pháp của bộ truy vấn của bạn Participantđể thay thế.

Tóm lại, những gì bạn muốn làm được đưa ra bởi:

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

Một ví dụ đầy đủ như sau:

  1. Tạo 2 Events:

    event1 = Event.objects.create(title='event1')
    event2 = Event.objects.create(title='event2')
  2. Thêm Participants vào chúng:

    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. Nhóm tất cả các Participants theo eventtrường của chúng :

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

    Ở đây cần có sự khác biệt:

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

    Điều đã .values.distinctđang làm ở đây là họ đang tạo hai nhóm Participantđược nhóm theo phần tử của họ event. Lưu ý rằng những thùng chứa Participant.

  4. Sau đó, bạn có thể chú thích các nhóm đó vì chúng chứa nhóm gốc Participant. Ở đây chúng tôi muốn đếm số lượng Participant, điều này được thực hiện đơn giản bằng cách đếm ids của các phần tử trong các nhóm đó (vì chúng là Participant):

    Participant.objects\
        .values('event')\
        .distinct()\
        .annotate(models.Count('id'))
    > <QuerySet [{'event': 1, 'id__count': 10}, {'event': 2, 'id__count': 50}]>
  5. Cuối cùng, bạn chỉ muốn Participantvới một thực is_paidthể True, bạn có thể chỉ cần thêm một bộ lọc vào trước biểu thức trước đó và điều này mang lại biểu thức được hiển thị ở trên:

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

Hạn chế duy nhất là bạn phải truy xuất Eventsau đó vì bạn chỉ có idtừ phương pháp trên.


2

Kết quả tôi đang tìm kiếm:

  • Những người (người được giao) đã thêm nhiệm vụ vào báo cáo. - Tổng số người duy nhất
  • Những người có nhiệm vụ được thêm vào báo cáo nhưng đối với nhiệm vụ có khả năng thanh toán chỉ hơn 0.

Nói chung, tôi sẽ phải sử dụng hai truy vấn khác nhau:

Task.objects.filter(billable_efforts__gt=0)
Task.objects.all()

Nhưng tôi muốn cả hai trong một truy vấn. Vì thế:

Task.objects.values('report__title').annotate(withMoreThanZero=Count('assignee', distinct=True, filter=Q(billable_efforts__gt=0))).annotate(totalUniqueAssignee=Count('assignee', distinct=True))

Kết quả:

<QuerySet [{'report__title': 'TestReport', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}, {'report__title': 'Utilization_Report_April_2019', 'withMoreThanZero': 37, 'totalUniqueAssignee': 50}]>
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.