Class-Based Views در جنگو منطق یک view را به متدهای یک کلاس می‌شکند و با پشتیبانی از mixin و وراثت، بازاستفاده و تست پذیری را ساده می‌کند؛ در مقابل Function-Based Views ساده تر و صریح ترند. برای CRUD و لیست ها، Generic Views مثل ListView و CreateView اکثر کدهای تکراری را حذف می کنند و نقطه شروع بهینه ای هستند.

چه فرقی با Function-Based Views دارد؟

تفاوت اصلی این است که در FBV همه چیز در یک تابع است، اما در CBV رفتار بر اساس متدهای HTTP (مثل get و post) و hook ها (مثل get_queryset یا form_valid) جدا می شود. نتیجه:

  • سازمان دهی بهتر: منطق هر سناریو در متد مرتبط قرار می گیرد.
  • بازاستفاده با mixin: احراز هویت، صفحه بندی، مجوز و کش با یک وراثت اضافه می شود.
  • قابل توسعه: فقط متدی را که لازم دارید override می کنید.
  • منحنی یادگیری: درک چرخه dispatch و ترتیب وراثت کمی زمان می برد.
  • صراحت: FBV برای سناریوهای خیلی ساده خواناتر است.
  • کارایی: تفاوت چشمگیری در عملکرد بین CBV و FBV در اکثر پروژه ها ندارید؛ انتخاب بیشتر معماری و نگهداری را تحت تاثیر می گذارد.

نمونه فوری: از یک CBV ساده تا اتصال در urls

هدف: یک صفحه ساده که روی GET پاسخ می دهد.

# views.py
from django.http import HttpResponse
from django.views import View

class HelloView(View):
    def get(self, request):
        return HttpResponse("سلام از CBV!")
# urls.py
from django.urls import path
from .views import HelloView

urlpatterns = [
    path("hello/", HelloView.as_view(), name="hello"),
]

متد as_view نمونه ای از کلاس را برای سیستم URL قابل فراخوانی می کند. اگر POST یا متدهای دیگر را هم لازم دارید، همان متد را در کلاس پیاده سازی کنید.

مقایسه کوتاه با FBV در یک سناریوی واقعی

فرض کنید مدل Post دارید و می خواهید لیست پست های منتشر شده را نمایش دهید.

# models.py
from django.db import models

class Post(models.Model):
    title = models.CharField(max_length=200)
    body = models.TextField()
    slug = models.SlugField(unique=True)
    is_published = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

    def __str__(self):
        return self.title
# FBV
from django.shortcuts import render
from .models import Post

def post_list(request):
    qs = Post.objects.filter(is_published=True).order_by("-created_at")
    return render(request, "blog/post_list.html", {"posts": qs})
# CBV با Generic ListView
from django.views.generic import ListView

class PostListView(ListView):
    model = Post
    queryset = Post.objects.filter(is_published=True)
    ordering = "-created_at"
    context_object_name = "posts"
    paginate_by = 10
    template_name = "blog/post_list.html"

در نسخه CBV صفحه بندی، مرتب سازی و نام کانتکست به شکل خوانا و قابل ترکیب با mixin ها تنظیم شده است.

Generic Views رایج و سفارشی سازی آن ها

ListView: لیست داده ها با صفحه بندی

زمانی که می خواهید لیست رکوردها را با امکان صفحه بندی و مرتب سازی نشان دهید، ListView بهترین گزینه است.

from django.views.generic import ListView
from .models import Post

class PostListView(ListView):
    model = Post
    paginate_by = 10
    context_object_name = "posts"

    def get_queryset(self):
        qs = Post.objects.filter(is_published=True)
        q = self.request.GET.get("q")
        return qs.filter(title__icontains=q) if q else qs

با override کردن get_queryset فیلتر پویا بر اساس ورودی کاربر اضافه می کنید. نتیجه را با اضافه کردن پارامتر q به آدرس تست کنید.

DetailView: نمایش جزئیات

from django.views.generic import DetailView

class PostDetailView(DetailView):
    model = Post
    slug_field = "slug"
    slug_url_kwarg = "slug"
    context_object_name = "post"

    def get_queryset(self):
        return Post.objects.filter(is_published=True)
# urls.py
path("posts/<slug:slug>/", PostDetailView.as_view(), name="post_detail")

CreateView و UpdateView: فرم ها و ذخیره سازی

برای ساخت و ویرایش رکوردها از CreateView و UpdateView استفاده کنید. توجه کنید مقداردهی مسیر بازگشت را یا با success_url یا با get_success_url مشخص کنید. اگر مدل شما get_absolute_url داشته باشد، همان استفاده می شود.

from django.urls import reverse_lazy, reverse
from django.views.generic import CreateView, UpdateView
from .models import Post

class PostCreateView(CreateView):
    model = Post
    fields = ["title", "body", "slug", "is_published"]
    template_name = "blog/post_form.html"
    success_url = reverse_lazy("post_list")  # مناسب برای سطح کلاس

class PostUpdateView(UpdateView):
    model = Post
    fields = ["title", "body", "is_published"]
    template_name = "blog/post_form.html"

    def get_success_url(self):
        return reverse("post_detail", kwargs={"slug": self.object.slug})

DeleteView: حذف ایمن با تایید

from django.views.generic import DeleteView

class PostDeleteView(DeleteView):
    model = Post
    template_name = "blog/post_confirm_delete.html"
    success_url = reverse_lazy("post_list")

در قالب تایید حذف، فرم با method=post و csrf_token لازم است تا حذف ناخواسته با GET رخ ندهد.

Mixin ها و کنترل دسترسی در CBV

با mixin ها بدون تکرار کد، قواعد مشترک را اعمال کنید. ترتیب وراثت مهم است: mixin ها باید قبل از کلاس generic بیایند.

from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin

class SecurePostUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
    model = Post
    fields = ["title", "body", "is_published"]
    permission_required = "blog.change_post"
    raise_exception = True  # به جای ریدایرکت، 403 می دهد
    template_name = "blog/post_form.html"

برای اعمال decorator ها (مثل کش) روی CBV از method_decorator با نام dispatch استفاده کنید.

from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_page

@method_decorator(cache_page(60), name="dispatch")
class CachedPostListView(ListView):
    model = Post
    paginate_by = 20

چه زمانی CBV و چه زمانی FBV؟

  • CRUD، لیست، جزئیات، فرم های استاندارد: CBV با Generic ها سریع تر و تمیزتر است.
  • رفتار بسیار سفارشی یا یکبار مصرف کوچک: FBV صریح تر و کم هزینه تر است.
  • نیاز به ترکیب چند قابلیت مشترک (احراز هویت، کش، مجوز): CBV با mixin ها انتخاب بهتری است.
  • تیم تازه کار با جنگو: شروع با FBV برای درک چرخه درخواست، سپس مهاجرت بخش های تکراری به CBV.

خطاهای رایج و راه حل آن ها

  • فراموشی as_view در urls: همواره کلاس را با as_view ثبت کنید.
  • ترتیب وراثت اشتباه: mixin ها باید سمت چپ و قبل از کلاس اصلی بیایند. مثلا LoginRequiredMixin قبل از ListView.
  • override کردن get بدون فراخوانی super در موارد لازم: برای افزودن داده اضافی به context از get_context_data استفاده کرده و super را صدا بزنید.
  • success_url در سطح کلاس با reverse به جای reverse_lazy: برای ارجاع در سطح کلاس از reverse_lazy استفاده کنید تا در زمان import ارزیابی نشود.
  • عدم انطباق نام پارامتر url با slug_url_kwarg یا pk_url_kwarg: نام ها را هماهنگ نگه دارید تا DetailView رکورد درست را بیابد.
  • اتکا به رفتار ضمنی: template_name را شفاف بنویسید تا از حدس اشتباه مسیر قالب جلوگیری شود.
# افزودن داده به context به شکل درست
class PostDetailView(DetailView):
    model = Post
    context_object_name = "post"
    template_name = "blog/post_detail.html"

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        ctx["related"] = Post.objects.filter(is_published=True).exclude(pk=self.object.pk)[:5]
        return ctx

روش بررسی نتیجه و تست سریع

برای اطمینان از عملکرد، مسیرها و کدهای پاسخ را تست کنید.

# tests.py
from django.test import TestCase
from django.urls import reverse
from .models import Post

class PostViewsTests(TestCase):
    def setUp(self):
        self.post = Post.objects.create(title="A", body="B", slug="a", is_published=True)

    def test_list_view_ok(self):
        url = reverse("post_list")
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)
        self.assertContains(resp, "A")

    def test_detail_view_ok(self):
        url = reverse("post_detail", kwargs={"slug": "a"})
        resp = self.client.get(url)
        self.assertEqual(resp.status_code, 200)

این تست ها اطمینان می دهند که Generic Views شما درست به مدل متصل شده و قالب ها را رندر می کنند.

پیش نیازها و نکات امنیتی کوتاه

  • برای فرم ها، csrf_token را در قالب فراموش نکنید.
  • در CreateView و UpdateView فیلدهایی را که مجاز است کاربر پر کند به صورت محدود در fields یا form_class تعریف کنید تا از mass assignment ناخواسته جلوگیری شود.
  • در DeleteView حذف را فقط با POST تایید کنید.
  • Queryset های حساس را با فیلتر بر اساس کاربر محدود کنید؛ مثلا فقط نویسنده بتواند پست خود را ویرایش کند.
class MyPostsUpdateView(LoginRequiredMixin, UpdateView):
    model = Post
    fields = ["title", "body", "is_published"]

    def get_queryset(self):
        # فقط پست های متعلق به کاربر جاری
        return Post.objects.filter(author=self.request.user)

گام بعدی

اگر قصد دارید CBV و Generic ها را در یک پروژه واقعی فروشگاه آنلاین به کار بگیرید، مسیر منطقی بعدی کار با URL ها، مدل ها و فرم ها در یک پروژه کامل است. پیشنهاد می کنیم از آموزش جنگو رایگان (فروشگاه اینترنتی) + سورس استفاده کنید تا مفاهیم این صفحه را در عمل ببینید.

همچنین اگر به اجرای کارهای پس زمینه علاقه دارید، راهنمای آموزش Celery در جنگو: اجرای کارهای پس زمینه و وظایف زمان بندی شده مکمل خوبی برای معماری پروژه شماست.

نتیجه نهایی

برای صفحات استاندارد و CRUD، از Class-Based Views همراه با Generic ها شروع کنید و فقط وقتی رفتار بسیار خاصی لازم است سراغ FBV بروید. با چند override کوچک (get_queryset، get_context_data، form_valid) و چند mixin، به معماری تمیزتر و کد قابل نگهداری تری می رسید.