Viết hàm view riêng cho từng trang danh sách và trang chi tiết khiến bạn lặp đi lặp lại cùng một mẫu: lấy queryset, sắp xếp, phân trang, hoặc gọi get_object_or_404 rồi render template. ListView và DetailView là hai class-based view có sẵn của Django làm đúng những việc đó — bạn chỉ cần khai báo model, queryset và template, phần còn lại framework lo. Bài này thay hai hàm view thủ công của app blog bằng hai class này.
Vấn đề
Một app blog thường có hai trang lặp đi lặp lại: danh sách bài viết và chi tiết một bài viết. Viết bằng hàm view, cả hai đều dài dòng: lấy queryset, lọc bài đã xuất bản, sắp xếp, tự tạo Paginator rồi lấy trang hiện tại, hoặc gọi get_object_or_404 theo slug. Viết ba, bốn trang kiểu này là bạn sao chép cùng một mẫu nhiều lần, và mỗi lần sửa logic phân trang hay lọc bài viết lại phải sửa ở nhiều chỗ.
# views.py — hàm view thủ công, lặp lại cùng một mẫufrom django.core.paginator import Paginatorfrom django.shortcuts import get_object_or_404, renderfrom .models import Postdef post_list(request): posts = Post.objects.filter(published=True).order_by("-created_at") paginator = Paginator(posts, 10) page = paginator.get_page(request.GET.get("page")) return render(request, "blog/post_list.html", {"page_obj": page})def post_detail(request, slug): post = get_object_or_404(Post, slug=slug, published=True) return render(request, "blog/post_detail.html", {"post": post})
Giải pháp
ListView và DetailView gói gọn đúng mẫu đó. ListView tự lấy queryset, tự phân trang và đưa page_obj, paginator, is_paginated vào template. DetailView tự tìm object theo slug hoặc khóa chính, và tự trả 404 khi không tìm thấy. Bạn chỉ khai báo những gì khác biệt: model nào, lọc ra sao, template nào, mỗi trang bao nhiêu mục.
# views.py — class-based view, khai báo thay vì viết lại logicfrom django.views.generic import DetailView, ListViewfrom .models import Postclass PostListView(ListView): model = Post queryset = Post.objects.filter(published=True).order_by("-created_at") template_name = "blog/post_list.html" context_object_name = "posts" paginate_by = 10class PostDetailView(DetailView): model = Post queryset = Post.objects.filter(published=True) template_name = "blog/post_detail.html" context_object_name = "post"
# urls.py — nhớ gọi .as_view() khi khai báo routefrom django.urls import pathfrom .views import PostDetailView, PostListViewurlpatterns = [ path("", PostListView.as_view(), name="post-list"), path("<slug:slug>/", PostDetailView.as_view(), name="post-detail"),]
Trong template danh sách, bạn dùng posts để lặp và page_obj.has_next để vẽ nút sang trang, vì paginate_by đã bật phân trang sẵn. Trong template chi tiết, biến post chính là object DetailView tìm được. Nếu cần lọc theo tham số URL — ví dụ chỉ liệt kê bài của một chuyên mục — bạn override get_queryset() và đọc self.kwargs thay vì khai báo queryset cố định. Khi logic một trang phức tạp hơn mức class generic lo được, hãy quay lại hàm view thay vì cố nhét mọi thứ vào ListView.
Áp dụng vào dự án của bạn
Mở app Django của bạn và tìm các hàm view chỉ làm ba việc: lấy danh sách, sắp xếp, phân trang — hoặc lấy một object theo slug rồi render. Thay từng hàm bằng ListView hoặc DetailView, chạy lại test của app để chắc chắn template nhận đúng biến context, rồi mới chuyển sang trang tiếp theo. Những trang còn lại — form, xử lý POST, logic nhiều nguồn dữ liệu — cứ giữ hàm view như cũ.
Bài viết liên quan
- Django: để model tự biết link của nó bằng get_absolute_url(), khỏi hardcode URL khắp nơi
- Django: đặt related_name và on_delete đúng cách để quan hệ không còn rối
- Django: sửa lỗi N+1 query bằng select_related trong 5 phút
Câu hỏi thường gặp
Khi nào nên dùng hàm view thay vì ListView hoặc DetailView?
Khi trang cần logic phức tạp: xử lý form POST, kết hợp nhiều nguồn dữ liệu, hoặc tính toán riêng trước khi render. Class generic chỉ gọn khi trang đúng mẫu lấy danh sách hoặc lấy một object rồi hiển thị.
Không khai báo template_name thì ListView dùng template nào?
Django tự tìm theo quy ước <app>/<model>_list.html cho ListView và <app>/<model>_detail.html cho DetailView, biến context mặc định là object_list và object. Khai báo rõ template_name và context_object_name giúp người đọc code sau này không phải đoán.
Làm sao lọc danh sách theo tham số trên URL?
Override get_queryset() và dùng self.kwargs để đọc tham số, ví dụ lọc Post theo category__slug=self.kwargs[“slug”]. Phân trang và sắp xếp của ListView vẫn giữ nguyên.
Phân trang trong template dùng biến gì?
Khi bạn đặt paginate_by, ListView đưa sẵn page_obj, paginator và is_paginated vào context. Dùng page_obj.has_next và page_obj.next_page_number để vẽ nút “Trang sau”, không cần tự tạo Paginator nữa.
URL dùng khóa chính thay vì slug thì sao?
Mặc định DetailView đọc tham số pk trên URL để tìm theo khóa chính. Nếu URL dùng slug, nó đọc tham số slug và tìm theo trường slug — đổi tên trường bằng slug_field khi cần.
💬 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.
