12/08/2018, 14:19

Django Aggregation (Part II)

Trước đây, tôi đã có một loạt bài dịch về QuerySet trong Django: A Survey On QuerySet In Django (Part I) A Survey On QuerySet In Django (Part II) Các bài viết này trình bày về cách chúng ta sử dụng các câu query để thực hiện CRUD trong Django. Tuy nhiên, đôi khi chúng ta sẽ cần lấy ra các giá ...

Trước đây, tôi đã có một loạt bài dịch về QuerySet trong Django:

A Survey On QuerySet In Django (Part I)

A Survey On QuerySet In Django (Part II)

Các bài viết này trình bày về cách chúng ta sử dụng các câu query để thực hiện CRUD trong Django. Tuy nhiên, đôi khi chúng ta sẽ cần lấy ra các giá trị được dẫn xuất thông qua các phép toán tập hợp (Aggregation Operation). Và bài viết này sẽ hướng dẫn chúng ta giải quyết vấn đề đó.

Giả sử chúng ta có các model như bên dưới. Các model này được sử dụng để theo dõi việc kiểm kê ở một chuỗi cửa hàng sách online:

from django.db import models

class Author(models.Model):
    name = models.CharField(max_length=100)
    age = models.IntegerField()

class Publisher(models.Model):
    name = models.CharField(max_length=300)
    num_awards = models.IntegerField()

class Book(models.Model):
    name = models.CharField(max_length=300)
    pages = models.IntegerField()
    price = models.DecimalField(max_digits=10, decimal_places=2)
    rating = models.FloatField()
    authors = models.ManyToManyField(Author)
    publisher = models.ForeignKey(Publisher)
    pubdate = models.DateField()

class Store(models.Model):
    name = models.CharField(max_length=300)
    books = models.ManyToManyField(Book)
    registered_users = models.PositiveIntegerField()

Joins and aggregates

Cho đến bây giờ, chúng ta đã thực hiện các aggregate qua field của model cần query. Tuy nhiên, đôi lúc giá trị bạn muốn aggregate lại thuộc về một model khác có quan hệ với model hiện tại đang query.

Khi xác định field được aggregate trong một hàm aggregate, Django sẽ cho phép bạn sử dụng ký hiệu double underscore ("__") để tham chiếu tới các field quan hệ trong filter. Django sau đó sẽ xử lý các kết nối bảng để lấy ra và aggregate các giá trị quan hệ.

Ví dụ, để tìm khoảng giá của các cuốn sách trong các store, bạn có thể làm như sau:

from django.db.models import Max, Min
Store.objects.annotate(min_price=Min('books__price'), max_price=Max('books__price'))

Đoạn code này sẽ yêu cầu Django lấy model Store join với model Book và thực hiện aggregate ở field price của model Book để lấy ra các giá trị max, min.

Các nguyên tắc tương tự được áp dụng với mệnh đề aggregate(). Nếu bạn muốn biết giá của các cuốn sách rẻ nhất và đắt nhất trong tất cả các cửa hàng, ta sử dụng aggregate sau:

Store.objects.aggregate(min_price=Min('books__price'), max_price=Max('books__price'))

Còn khi bạn muốn biết tuổi của tác giả trẻ nhất trong số các tác giả viết ra sách được bán trong các store, bạn có thể thực hiện câu query sau:

Store.objects.aggregate(youngest_age=Min('books__authors__age'))

Following relationships backwards

Để tham chiếu ngược, chúng ta sử dụng double underscore ("__").

Ví dụ, chúng ta có thể lấy ra số cuốn sách của mỗi nhà xuất bản bằng câu query sau:

from django.db.models import Count, Min, Sum, Avg

Publisher.objects.annotate(Count('book'))

Mỗi Publisher trong QuerySet trả về sẽ có thêm attribute book__count.

Chúng ta cũng có thể tìm được cuốn sách cũ nhất trong tất cả các cuốn sách của các nhà xuất bản:

Publisher.objects.aggregate(oldest_pubdate=Min('book__pubdate'))

Dictionary trả về sẽ có một key là oldest_pubdate. Nếu không xác định alias từ trước, key đó sẽ có tên là book__pubdate__min.

Ở các ví dụ trên, chúng ta đã áp dụng với quan hệ khóa ngoài. Ngoài khóa ngoài, chúng ta có thể áp dụng aggregateannotation với quan hệ many-to-many. Ví dụ, để thống kê tổng số trang của các cuốn sách mà một tác giả từng viết hoặc từng là đồng tác giả, ta sử dụng câu query sau:

Author.objects.annotate(total_pages=Sum('book__pages'))

Mỗi Author trong QuerySet trả về sẽ có thêm một attribute tên total_pages.

Một ví dụ khác để lấy ra rating trung bình dành cho các cuốn sách của một tác giả:

Author.objects.aggregate(average_rating=Avg('book__rating'))

Aggregations and other QuerySet clauses

filter() and exclude()

Các aggregate có thể tham gia vào filter.

Khi được sử dụng với mệnh đề annotate(), filter sẽ giới hạn số lượng object mà annotate sẽ tính toán. Ví dụ, bạn có thể tạo ra một list các cuốn sách có tên bắt đầu là "Django", kèm với đó là số lượng tác giả của mỗi cuốn sách:

from django.db.models import Count, Avg

Book.objects.filter(name__startswith="Django").annotate(num_authors=Count('authors'))

Tương tự, khi được sử dụng với mệnh đề aggregate(), filter sẽ giới hạn số lượng object aggregate sẽ tính toán. Ví dụ, bạn có thể tính giá trung bình của các cuốn sách có title bắt đầu bằng "Django":

Book.objects.filter(name__startswith="Django").aggregate(Avg('price'))

Filter on annotations

Các giá trị đã được annotate có thể được filter. Các alias sinh ra bởi annotate được sử dụng trong mệnh đề filter()exlucde() như bình thường, tức là như với các model field.

Ví dụ, để sinh ra một list các cuốn sách có nhiều hơn một tác giả, ta sử dụng câu query sau:

Book.objects.annotate(num_authors=Count('authors')).filter(num_authors__gt=1)

Order of annotate() and filter() clauses

Khi viết một câu query liên quan đến cả annotate()filter(), chúng ta cần đặc biệt quan tâm tới thứ tự của các mệnh đề đó.

Nói một cách đơn giản nhất, filter()annotate() không có tính giao hoán.

Giả sử ta có:

  • Publisher A có hai cuốn sách với rating là 4 và 5

  • Publisher B có hai cuốn sách với rating 1 và 4

  • Publisher C có một cuốn sách với rating 1

Dưới đây là một ví dụ với aggregate Count:

a, b = Publisher.objects.annotate(num_books=Count('book', distinct=True)).filter(book__rating__gt=3.0)
a, a.num_books
=> (<Publisher: A>, 2)

>>> b, b.num_books
=> (<Publisher: B>, 2)

a, b = Publisher.objects.filter(book__rating__gt=3.0).annotate(num_books=Count('book'))
a, a.num_books
=> (<Publisher: A>, 2)

b, b.num_books
=> (<Publisher: B>, 1)

Cả hai câu query đều trả về một list các publisher có ít nhất một cuốn sách với rating trên 3.0, do đó publisher C bị loại trừ.

Ở câu query đầu tiên, mệnh đề filter() xuất hiện sau nên không ảnh hưởng đến mệnh đề annotate(). Còn ở câu query thứ hai, mệnh đề filter() xuất hiện trước nên đã ảnh hưởng đến mệnh đề annotate().

Đây là một ví dụ khác với aggregate Avg:

a, b = Publisher.objects.annotate(avg_rating=Avg('book__rating')).filter(book__rating__gt=3.0)
a, a.avg_rating
=> (<Publisher: A>, 4.5)  # (5+4)/2

b, b.avg_rating
=> (<Publisher: B>, 2.5)  # (1+4)/2

a, b = Publisher.objects.filter(book__rating__gt=3.0).annotate(avg_rating=Avg('book__rating'))
a, a.avg_rating
=> (<Publisher: A>, 4.5)  # (5+4)/2

b, b.avg_rating
=> (<Publisher: B>, 4.0)  # 4/1 (book with rating 1 excluded)

Phần II của loạt bài viết về Django Aggreagation xin được dừng lại tại đây             </div>
            
            <div class=

0