Khi một model cần thêm vài trường tùy chọn — theme giao diện, ngôn ngữ, bật/tắt thông báo — cách gọn nhất không phải thêm cột mới cho mỗi tùy chọn, mà là dùng JSONField của Django: một cột duy nhất lưu dict, vẫn truy vấn được bằng ORM, và thêm key mới không cần migration. Cách này áp dụng khi dữ liệu linh hoạt, ít khi lọc phức tạp và không cần ràng buộc chặt ở tầng database; còn dữ liệu quan trọng, cần join hoặc ràng buộc, hãy dùng cột riêng.
Vấn đề
Hãy tưởng tượng bảng hồ sơ người dùng (UserProfile) cứ phình ra theo thời gian: đầu tiên là theme, rồi language, rồi notifications_email, rồi notifications_sms. Mỗi tùy chọn mới là một cột mới, một migration mới, và một model ngày càng khó đọc. Cách chữa cháy phổ biến khác cũng không khá hơn: lưu chuỗi JSON vào TextField rồi json.loads() mỗi khi đọc. Cách này khiến bạn không thể lọc trực tiếp trong database — muốn tìm người dùng chọn theme tối, bạn phải kéo toàn bộ bản ghi về Python rồi lọc tay.
Giải pháp
Khai báo một trường JSONField với giá trị mặc định là dict rỗng:
from django.db import modelsclass UserProfile(models.Model): user = models.OneToOneField("auth.User", on_delete=models.CASCADE) preferences = models.JSONField(default=dict, blank=True)
Lưu ý: dùng default=dict, không dùng default={}. Một dict dùng chung cho mọi bản ghi sẽ khiến thay đổi ở bản ghi này lan sang bản ghi khác.
Tạo và truy vấn đều viết như code Python bình thường:
profile = UserProfile.objects.create( user=user, preferences={ "theme": "dark", "language": "vi", "notifications": {"email": True, "sms": False}, },)# Lọc theo key ở cấp ngoàidark_users = UserProfile.objects.filter(preferences__theme="dark")# Lọc theo key lồng nhau, dùng __ để đi sâuemail_on = UserProfile.objects.filter(preferences__notifications__email=True)# Chỉ lấy bản ghi có key nào đóhas_theme = UserProfile.objects.filter(preferences__has_key="theme")
Muốn đổi một key mà không ghi đè toàn bộ dict, hãy sao chép rồi gán lại, sau đó lưu đúng cột đó:
prefs = dict(profile.preferences)prefs["theme"] = "light"profile.preferences = prefsprofile.save(update_fields=["preferences"])
Đừng sửa dict tại chỗ rồi gọi save() và hy vọng Django tự phát hiện thay đổi — cách sao chép và gán lại ở trên luôn chắc chắn. Nếu bạn lọc theo JSON thường xuyên trên PostgreSQL, hãy thêm GinIndex cho cột đó để truy vấn không phải quét toàn bảng.
Áp dụng vào dự án của bạn
JSONField hợp nhất với những dữ liệu “mềm”: tùy chọn giao diện, cấu hình thông báo, metadata của sản phẩm, hay cấu hình riêng theo từng tenant trong ứng dụng đa khách hàng. Quy tắc thực tế: nếu một key bắt đầu xuất hiện trong mọi truy vấn quan trọng, cần ràng buộc duy nhất, hoặc cần join với bảng khác, hãy tách nó thành cột riêng. Còn lại, cứ để nó sống trong JSONField cho gọn.
Bài viết liên quan
- Django: sửa lỗi N+1 query bằng select_related trong 5 phút
- Django: queryset nhẹ hơn trong 5 phút bằng values() và values_list()
- Django: gán nhãn và tính toán theo điều kiện ngay trong query bằng Case/When, khỏi vòng lặp Python
Câu hỏi thường gặp
JSONField lưu được những kiểu dữ liệu nào?
Lưu được dict, list, chuỗi, số, True/False và None — tức mọi thứ JSON biểu diễn được. Không lưu trực tiếp datetime hay object Python tùy ý; hãy chuyển chúng thành chuỗi hoặc số trước.
JSONField khác gì TextField chứa chuỗi JSON?
TextField chỉ lưu chuỗi, database không hiểu cấu trúc bên trong nên không lọc theo key được. JSONField được database hiểu là JSON thật, vì vậy bạn lọc, kiểm tra key tồn tại và (trên PostgreSQL) đánh index được.
Thêm key mới có cần migration không?
Không. Thêm, đổi tên hay bỏ một key trong dict không đổi schema, nên không cần migration. Bạn chỉ cần migration khi thêm, đổi hay xóa chính cột JSONField, hoặc thay đổi index của nó.
Khi nào không nên dùng JSONField?
Khi dữ liệu cần khóa ngoại, ràng buộc duy nhất, kiểm tra NOT NULL chặt chẽ, hoặc thống kê phức tạp trên chính trường đó. Những trường hợp này xứng đáng có cột riêng với kiểu rõ ràng.
💬 Bạn đã thử cách này trong dự án của mình chưa? Nếu gặp lỗi hoặc có cách làm gọn hơn, để lại bình luận bên dưới — mình đọc hết và sẽ bổ sung những ý hay vào bài.
