prefetch_related() mặc định lấy toàn bộ quan hệ liên quan, kể cả những bản ghi bạn sẽ không bao giờ dùng tới. Khi chỉ cần một tập con — ví dụ chỉ những cuốn sách đã xuất bản của mỗi tác giả — hãy dùng Prefetch() với queryset tùy chỉnh để database lọc ngay từ đầu, thay vì tải hết rồi lọc lại trong Python. Cách này áp dụng khi quan hệ là ManyToMany hoặc quan hệ ngược, và bạn biết trước điều kiện lọc khi viết query.

Vấn đề

Giả sử trang danh sách tác giả chỉ hiển thị sách đã xuất bản. Cách viết quen thuộc là prefetch toàn bộ rồi lọc trong Python:

authors = Author.objects.prefetch_related("books")
for author in authors:
published = [b for b in author.books.all() if b.is_published]

Đoạn code này chạy đúng nhưng lãng phí: mọi cuốn sách nháp vẫn bị tải về, chiếm RAM và băng thông chỉ để rồi bị loại bỏ. Tệ hơn, nếu bạn quên prefetch_related, mỗi lần chạm vào author.books.all() lại sinh thêm một query — đúng dạng lỗi N+1 mà prefetch sinh ra để giải quyết. Vấn đề không nằm ở việc lọc, mà ở chỗ dữ liệu thừa đã về tới Python trước khi bị lọc.

Giải pháp

Dùng Prefetch() để gắn điều kiện lọc ngay vào query prefetch:

from django.db.models import Prefetch
authors = Author.objects.prefetch_related(
Prefetch(
"books",
queryset=Book.objects.filter(is_published=True).order_by("-published_at"),
to_attr="published_books",
)
)
for author in authors:
for book in author.published_books:
print(author.name, book.title)

Toàn bộ công việc chỉ tốn hai query: một lấy tác giả, một lấy đúng những cuốn sách đã xuất bản của tất cả tác giả. to_attr="published_books" lưu kết quả thành một list sẵn trên mỗi tác giả, nên vòng lặp không phát sinh thêm query nào. Lưu ý hai điểm: thứ nhất, published_books là list Python, không còn là queryset nên không gọi thêm .filter() lên nó được; thứ hai, author.books.all() vẫn là cache riêng và sẽ query lại nếu bạn chạm vào, vì vậy hãy đặt tên to_attr rõ ràng và dùng nhất quán trong template lẫn view.

Áp dụng vào dự án của bạn

Hãy rà lại những chỗ bạn đang prefetch rồi lọc trong Python: trang hồ sơ chỉ hiện đơn hàng đã giao, dashboard chỉ đếm bình luận đã duyệt, email tổng kết chỉ lấy bài mới trong tuần. Mỗi chỗ như vậy là một ứng viên cho Prefetch() có điều kiện. Bạn cũng có thể lồng nhiều Prefetch() trong một lần gọi, và kết hợp select_related() bên trong queryset con khi cần.

Bài viết liên quan

Câu hỏi thường gặp

Prefetch() khác prefetch_related(“books”) ở điểm nào?

prefetch_related("books") là dạng rút gọn, lấy toàn bộ quan hệ. Prefetch() cho phép bạn chỉ định queryset riêng — lọc, sắp xếp, thậm chí select_related() bên trong — và đặt tên thuộc tính nhận kết quả bằng to_attr.

Dùng to_attr rồi còn lọc tiếp được không?

Không theo kiểu queryset, vì to_attr lưu kết quả dưới dạng list. Nếu cần lọc thêm theo điều kiện động, hãy lọc list đó trong Python hoặc viết một Prefetch() khác với điều kiện phù hợp ngay từ đầu.

Prefetch() có dùng cho ForeignKey được không?

Không cần. Quan hệ xuôi ForeignKey và OneToOne dùng select_related(). Prefetch() chỉ dành cho ManyToMany và quan hệ ngược, nơi Django phải chạy query riêng.

Khi nào không nên dùng Prefetch() có điều kiện?

Khi điều kiện lọc phụ thuộc vào từng đối tượng cha theo cách không biểu diễn được bằng một queryset chung, hoặc khi bạn thật sự cần toàn bộ quan hệ ở nhiều nơi khác nhau. Lúc đó prefetch toàn bộ một lần rồi tái sử dụng vẫn kinh tế hơn là prefetch riêng từng tập con.

💬 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.

Nguồn tham khảo