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ẫu
from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404, render
from .models import Post
def 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 logic
from django.views.generic import DetailView, ListView
from .models import Post
class 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 = 10
class 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 route
from django.urls import path
from .views import PostDetailView, PostListView
urlpatterns = [
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

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.

Nguồn tham khảo