Giới thiệu về biểu thức điều kiện trong Django
Trong quá trình phát triển ứng dụng web với Django, việc truy vấn dữ liệu thường yêu cầu các logic phức tạp hơn là những bộ lọc đơn giản. Biểu thức điều kiện cho phép chúng ta thực hiện các thao tác tương tự như cấu trúc IF-ELSE ngay trong câu lệnh SQL thông qua ORM mà không cần phải xử lý thủ công sau khi lấy dữ liệu về.
Bài viết này sẽ hướng dẫn cách sử dụng Case và When để tạo trường ảo, lọc dữ liệu, cập nhật có điều kiện và thực hiện các phép聚合 thống kê.
Thiết lập Model và dữ liệu mẫu
Đầu tiên, chúng ta cần định nghĩa một Model đại diện cho khách hàng. Hãy tạo file models.py trong ứng dụng của bạn với cấu trúc sau:
from django.db import models
class Customer(models.Model):
STANDARD = 'STD'
VIP = 'VIP'
PRO = 'PRO'
TIER_CHOICES = [
(STANDARD, 'Standard'),
(VIP, 'VIP'),
(PRO, 'Pro'),
]
full_name = models.CharField(max_length=100)
joined_at = models.DateField()
membership_tier = models.CharField(
max_length=5,
choices=TIER_CHOICES,
default=STANDARD,
)
Sau khi đã di chuyển Model vào database, chúng ta sẽ thêm một số bản ghi thử nghiệm thông qua Django shell để phục vụ cho các ví dụ sau:
from core.models import Customer
from datetime import date
Customer.objects.create(full_name="Nguyen Van A", joined_at=date(2020, 5, 10), membership_tier="STD")
Customer.objects.create(full_name="Tran Thi B", joined_at=date(2021, 8, 15), membership_tier="VIP")
Customer.objects.create(full_name="Le Van C", joined_at=date(2022, 2, 20), membership_tier="PRO")
Customer.objects.create(full_name="Pham Van D", joined_at=date(2023, 11, 5), membership_tier="PRO")
Sử dụng Case và When để tạo trường ảo
Giả sử chúng ta muốn phân loại khách hàng dựa trên mùa mà họ tham gia (mùa xuân, hạ, thu, đông) dựa trên tháng của trường joined_at. Thay vì lấy dữ liệu về rồi vòng lặp xử lý bằng Python, chúng ta có thể thực hiện ngay ở tầng database.
Đoạn mã dưới đây sẽ annotate thêm một trường season vào QuerySet:
from django.db.models import Case, When, Value
from core.models import Customer
qs = Customer.objects.annotate(
season=Case(
When(joined_at__month__in=[1, 2, 3], then=Value("Xuan")),
When(joined_at__month__in=[4, 5, 6], then=Value("Ha")),
When(joined_at__month__in=[7, 8, 9], then=Value("Thu")),
When(joined_at__month__in=[10, 11, 12], then=Value("Dong")),
default=Value("Khong xac dinh")
)
)
Trong đó, hàm Case hoạt động như một khối điều kiện chính, chứa nhiều mệnh đề When. Mỗi When kiểm tra một điều kiện cụ thể, nếu đúng thì trả về giá trị trong then. Hàm Value được sử dụng để bọc các giá trị hằng số (như chuỗi ký tự).
Nếu bạn muốn gán giá trị của một trường khác trong model thay vì một hằng số, bạn không cần dùng Value mà có thể truyền trực tiếp tên trường hoặc sử dụng biểu thức F:
from django.db.models import F
# Cách 1: Truyền trực tiếp tên trường
When(joined_at__month__in=[1, 2, 3], then="full_name")
# Cách 2: Sử dụng đối tượng F
When(joined_at__month__in=[1, 2, 3], then=F("full_name"))
Lọc dữ liệu dựa trên điều kiện phức tạp
Sau khi đã tạo được trường ảo season, chúng ta hoàn toàn có thể sử dụng nó để lọc dữ liệu. Ví dụ, chỉ lấy những khách hàng tham gia vào mùa Xuân:
spring_customers = Customer.objects.annotate(
season=Case(
When(joined_at__month__in=[1, 2, 3], then=Value("Xuan")),
When(joined_at__month__in=[4, 5, 6], then=Value("Ha")),
When(joined_at__month__in=[7, 8, 9], then=Value("Thu")),
When(joined_at__month__in=[10, 11, 12], then=Value("Dong")),
default=Value("Khong xac dinh")
)
).filter(season="Xuan")
Một tình huống khác phức tạp hơn: Giả sử chính sách lọc dữ liệu thay đổi tùy theo hạng thành viên. Nếu là VIP, chỉ lấy dữ liệu trong 30 ngày gần đây. Nếu là PRO, lấy dữ liệu trong 1 năm gần đây. Thông thường chúng ta sẽ dùng Q object:
from django.db.models import Q
from datetime import timedelta, date
today = date.today()
limit_vip = today - timedelta(days=30)
limit_pro = today - timedelta(days=365)
query_filter = (
Q(membership_tier=Customer.VIP) & Q(joined_at__gte=limit_vip)
) | (
Q(membership_tier=Customer.PRO) & Q(joined_at__gte=limit_pro)
)
data = Customer.objects.filter(query_filter)
Tuy nhiên, với Case và When, cú pháp sẽ gọn gàng hơn khi đặt logic vào ngay trong hàm lọc:
data = Customer.objects.filter(
joined_at__gte=Case(
When(membership_tier=Customer.VIP, then=limit_vip),
When(membership_tier=Customer.PRO, then=limit_pro),
default=today
)
)
Cập nhật dữ liệu có điều kiện
Không chỉ dùng để truy vấn, Case và When còn hỗ trợ cập nhật dữ liệu hàng loạt dựa trên điều kiện. Ví dụ, chúng ta muốn nâng hạng thành viên cho những người tham gia từ năm 2020 lên PRO, và năm 2021 xuống STANDARD:
Customer.objects.update(
membership_tier=Case(
When(joined_at__year=2020, then=Value(Customer.PRO)),
When(joined_at__year=2021, then=Value(Customer.STANDARD)),
default=Value(Customer.VIP)
)
)
Lưu ý rằng lệnh update trên sẽ tác động lên toàn bộ bảng. Nếu muốn giới hạn phạm vi cập nhật chỉ cho các năm cụ thể để tối ưu hiệu năng, hãy kết hợp thêm filter:
Customer.objects.filter(joined_at__year__in=[2020, 2021]).update(
membership_tier=Case(
When(joined_at__year=2020, then=Value(Customer.PRO)),
When(joined_at__year=2021, then=Value(Customer.STANDARD)),
default=Value(Customer.VIP)
)
)
Thống kê聚合 có điều kiện
Trong báo cáo, chúng ta thường cần đếm số lượng bản ghi thỏa mãn nhiều điều kiện khác nhau trong cùng một truy vấn. Django hỗ trợ điều này thông qua tham số filter trong các hàm aggregate như Count:
from django.db.models import Count, Q
stats = Customer.objects.aggregate(
count_std=Count('pk', filter=Q(membership_tier=Customer.STANDARD)),
count_vip=Count('pk', filter=Q(membership_tier=Customer.VIP)),
count_pro=Count('pk', filter=Q(membership_tier=Customer.PRO)),
)
Kết quả trả về sẽ là một dictionary chứa số lượng tương ứng cho từng hạng. Cách làm này tương đương với việc sử dụng COUNT(CASE WHEN...) trong SQL thuần.
Ngoài ra, nếu muốn lấy kết quả dưới dạng danh sách các nhóm, chúng ta có thể kết hợp values và annotate:
grouped_stats = Customer.objects.values("membership_tier").annotate(total=Count("membership_tier"))
Kết quả của cách này sẽ là một QuerySet chứa các dictionary, mỗi dictionary đại diện cho một nhóm hạng thành viên và tổng số lượng của nhóm đó.