تا الان سه روش برای نوشتن ویو در DRF یاد گرفته‌اید.

روش اول: Function-Based View با دکوریتور @api_view. ساده اما برای پروژه‌های بزرگ کافی نیست.

روش دوم: APIView. کنترل کامل روی منطق ویو. اما برای هر مدل باید چندین کلاس جداگانه بنویسید.

روش سوم: Generic Views مثل ListCreateAPIView. کدها خیلی کوتاه شدند. اما همچنان برای هر مدل به دو کلاس مجزا نیاز دارید. یکی برای لیست و ساختن، دیگری برای جزئیات و بروزرسانی و حذف.

حالا فرض کنید پروژه شما ده مدل داشته باشد. باید بیست کلاس بنویسید. هر کدام با مسیرهای جداگانه.

اینجا ViewSet وارد می‌شود.

یک ViewSet همه عملیات‌های CRUD را در یک کلاس جمع می‌کند. دیگر خبری از دو کلاس جداگانه نیست. فقط یک کلاس برای هر مدل.

اما این همه ماجرا نیست. ViewSetها با Router همراه می‌شوند. دیگر نیازی نیست urlpatterns را دستی پر کنید. Router خودکار مسیرهای مربوط به هر عملیات را می‌سازد.

در این درس، از ModelViewSet شروع می‌کنیم. سپس ReadOnlyModelViewSet را یاد می‌گیرید. بعد با DefaultRouter آشنا می‌شوید. در انتها هم یاد می‌گیرید چطور متدهای سفارشی مثل activate/ را با @action به ViewSet اضافه کنید.

اگر Generic Views را خوب فهمیده باشید، ViewSet برای شما یک ارتقاء طبیعی و شیرین خواهد بود.

ViewSet چیست و چه فرقی با Generic Views دارد؟

ViewSet یک کلاس در DRF است. کار آن شبیه Generic Views است، اما با یک تفاوت بزرگ.

Generic Views برای هر عملیات، یک کلاس جداگانه داشت. مثلاً ListCreateAPIView برای لیست و ساخت، و RetrieveUpdateDestroyAPIView برای جزئیات و بروزرسانی و حذف.

ViewSet اما همه این عملیات‌ها را در یک کلاس جمع می‌کند.

from rest_framework.viewsets import ModelViewSet
from .models import Article
from .serializers import ArticleSerializer

class ArticleViewSet(ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

با همین چند خط کد، شش عملیات در دسترس است:

  • list (GET لیست همه)
  • create (POST ساخت جدید)
  • retrieve (GET جزئیات یک آیتم)
  • update (PUT بروزرسانی کامل)
  • partial_update (PATCH بروزرسانی جزیی)
  • destroy (DELETE حذف)

در Generic Views، برای همین شش عملیات به دو کلاس جداگانه نیاز داشتید.

تفاوت اصلی با Generic Views

تفاوت اول: تعداد کلاس‌ها

Generic Views: برای هر مدل حداقل دو کلاس نیاز دارید.
ViewSet: برای هر مدل فقط یک کلاس نیاز دارید.

تفاوت دوم: نحوه تعریف مسیرها

Generic Views: باید برای هر کلاس یک مسیر در urlpatterns بنویسید.

urlpatterns = [
    path('articles/', ArticleListCreateView.as_view()),
    path('articles/<int:pk>/', ArticleDetailView.as_view()),
]

ViewSet: مسیرها را به Router می‌دهید. خودش همه را می‌سازد.

router.register('articles', ArticleViewSet)

تفاوت سوم: نامگذاری متدها

در Generic Views، نام متدها بر اساس متد HTTP است (get، post، put، delete).

در ViewSet، نام متدها بر اساس عملیات است (list، create، retrieve، update، destroy). خود DRF این متدها را به متدهای HTTP مناسب نگاشت می‌کند.

تفاوت چهارم: انعطاف در ترکیب

Generic Views: شما مجبورید از کلاس‌های آماده استفاده کنید یا خودتان با mixins ترکیب بسازید.

ViewSet: به صورت پیش‌فرض همه عملیات‌ها را دارد. اگر نیاز به زیرمجموعه‌ای از آن‌ها دارید، می‌توانید از ReadOnlyModelViewSet یا ترکیب mixins با GenericViewSet استفاده کنید.

جدول مقایسه سریع

ویژگی Generic Views ViewSet
تعداد کلاس برای یک مدل ۲ کلاس ۱ کلاس
تعریف مسیرها دستی با path خودکار با Router
نام متدها get, post, put, delete list, create, retrieve, update, destroy
قابلیت اضافه کردن متد سفارشی نیاز به کلاس جدید با @action در همان کلاس
مناسب برای پروژه‌های کوچک و متوسط پروژه‌های متوسط و بزرگ

چه موقع از ViewSet استفاده کنیم؟

ViewSet برای زمانی مناسب است که:

می‌خواهید کد خود را جمع‌وجورتر کنید

به همه عملیات‌های CRUD نیاز دارید

نمی‌خواهید برای هر مدل چندین کلاس بنویسید

از مزیت Router برای خودکارسازی مسیرها استفاده کنید

چه موقع از Generic Views استفاده کنیم؟

Generic Views هنوز هم جای خود را دارند. زمانی که:

فقط به یک یا دو عملیات خاص نیاز دارید (مثلاً فقط لیست و ساخت)

می‌خواهید کنترل بیشتری روی تک تک متدها داشته باشید

پروژه شما خیلی بزرگ نیست و چند کلاس اضافه دردسر ایجاد نمی‌کند

در زیرعنوان بعدی، با ModelViewSet آشنا می‌شوید. کامل‌ترین و پرکاربردترین نوع ViewSet در DRF.

ModelViewSet – کامل‌ترین ویو DRF

ModelViewSet یک کلاس آماده در DRF است. از GenericViewSet ارث‌بری می‌کند و پنج mixin را با هم ترکیب می‌کند.

این پنج mixin عبارتند از:

  • ListModelMixin برای لیست گرفتن
  • CreateModelMixin برای ساختن
  • RetrieveModelMixin برای جزئیات
  • UpdateModelMixin برای بروزرسانی
  • DestroyModelMixin برای حذف

نتیجه: یک کلاس که تمام عملیات‌های CRUD را پشتیبانی می‌کند.

کد مورد نیاز:

from rest_framework.viewsets import ModelViewSet
from .models import Article
from .serializers import ArticleSerializer

class ArticleViewSet(ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

تنها سه خط کد. بدون متد get، بدون post، بدون put، بدون delete. همه چیز آماده است.

شش عملیاتی که ModelViewSet در اختیار شما می‌گذارد

عملیات متد HTTP آدرس نمونه توضیح
list GET /articles/ لیست همه مقالات
create POST /articles/ ساخت مقاله جدید
retrieve GET /articles/1/ جزئیات مقاله با ID ۱
update PUT /articles/1/ بروزرسانی کامل مقاله ۱
partial_update PATCH /articles/1/ بروزرسانی جزیی مقاله ۱
destroy DELETE /articles/1/ حذف مقاله ۱

مقایسه با روش‌های قبلی

با APIView: برای همین شش عملیات، به دو کلاس و حدود ۵۰ خط کد نیاز داشتید.

با Generic Views: به دو کلاس (ListCreateAPIView و RetrieveUpdateDestroyAPIView) و حدود ۱۵ خط کد نیاز داشتید.

با ModelViewSet: فقط یک کلاس و کمتر از ۱۰ خط کد.

تنظیم مسیر با Router

برای استفاده از ModelViewSet، مسیرها را به Router می‌دهید:

from rest_framework.routers import DefaultRouter
from django.urls import path, include

router = DefaultRouter()
router.register('articles', ArticleViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
]

با این چند خط، تمام شش مسیر زیر به صورت خودکار ساخته می‌شوند:

  • GET /api/articles/
  • POST /api/articles/
  • GET /api/articles/{id}/
  • PUT /api/articles/{id}/
  • PATCH /api/articles/{id}/
  • DELETE /api/articles/{id}/

سفارشی‌سازی ModelViewSet

حتی با ModelViewSet هم می‌توانید رفتار پیش‌فرض را تغییر دهید.

تغییر queryset بر اساس کاربر:

def get_queryset(self):
    user = self.request.user
    if user.is_staff:
        return Article.objects.all()
    return Article.objects.filter(author=user.username)

تغییر Serializer بر اساس عملیات:

def get_serializer_class(self):
    if self.action == 'create':
        return CreateArticleSerializer
    return ArticleSerializer

self.action نام عملیاتی است که در حال اجراست. مثلاً 'list'، 'create'، 'retrieve'، 'update'، 'destroy'.

محدودیت‌های ModelViewSet

با وجود تمام قدرتی که دارد، ModelViewSet برای همه موارد مناسب نیست.

  • محدودیت اول: اگر فقط به دو عملیات نیاز دارید (مثلاً فقط لیست و جزئیات)، ModelViewSet عملیات‌های اضافی (ساخت، بروزرسانی، حذف) را هم در اختیار کاربر می‌گذارد. در این حالت بهتر است از ReadOnlyModelViewSet استفاده کنید.
  • محدودیت دوم: کنترل روی تک تک متدها سخت‌تر از APIView است. اگر نیاز به منطق خیلی خاص دارید، شاید APIView انتخاب بهتری باشد.
  • محدودیت سوم: مستندات خودکار Browsable API برای ModelViewSet همه شش عملیات را نشان می‌دهد. این ممکن است برای برخی APIها زیاد باشد.

تمرین برای شما

یک ModelViewSet برای مدل Product با فیلدهای name، price، stock بنویسید. سپس آن را در Router ثبت کنید و در مرورگر تست کنید.

در زیرعنوان بعدی، با ReadOnlyModelViewSet آشنا می‌شوید. نسخه سبک‌تری از ModelViewSet که فقط عملیات‌های خواندنی را شامل می‌شود.

ReadOnlyModelViewSet – برای APIهای فقط خواندنی

ReadOnlyModelViewSet نسخه سبک‌تری از ModelViewSet است. همانطور که از اسمش پیداست، فقط عملیات‌های خواندنی را شامل می‌شود.

این کلاس فقط دو عملیات دارد:

  • list (لیست گرفتن همه رکوردها)
  • retrieve (دریافت جزئیات یک رکورد)

هیچ عملیات نوشتنی (ساخت، بروزرسانی، حذف) در آن وجود ندارد.

کد مورد نیاز:

from rest_framework.viewsets import ReadOnlyModelViewSet
from .models import Article
from .serializers import ArticleSerializer

class ArticleReadOnlyViewSet(ReadOnlyModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

دقیقاً شبیه ModelViewSet، فقط کلاس پایه فرق کرده است.

چه زمانی از ReadOnlyModelViewSet استفاده کنیم؟

موقعیت اول: نمایش عمومی داده

فرض کنید یک سایت خبری دارید. کاربران باید بتوانند مقالات را ببینند، اما اجازه ساخت یا ویرایش ندارند.

ReadOnlyModelViewSet دقیقاً برای همین ساخته شده. همه می‌توانند بخوانند، هیچ کس نمی‌تواند بنویسد.

موقعیت دوم: پنل کاربری بدون اجازه تغییر

کاربر وارد پنل کاربری خود می‌شود. می‌تواند اطلاعات پروفایل خود را ببیند، اما نمی‌تواند آن را تغییر دهد. یا شاید تغییر از طریق فرم جداگانه انجام می‌شود و API فقط نمایش است.

موقعیت سوم: APIهای آماری و گزارشی

سیستم شما یک API برای نمایش آمار بازدید دارد. کاربران فقط باید آمار را ببینند. هیچ دلیلی برای اجازه تغییر وجود ندارد.

موقعیت چهارم: مرحله اول پیاده‌سازی

وقتی تازه شروع به ساختن یک API می‌کنید، ممکن است اول بخش خواندنی را بسازید و تست کنید. بعداً اگر نیاز شد، عملیات نوشتنی را اضافه کنید. اما در آن زمان مجبورید کلاس را از ReadOnlyModelViewSet به ModelViewSet تغییر دهید.

مقایسه مسیرهای ساخته شده

با ReadOnlyModelViewSet:

آدرس متد توضیح
/api/articles/ GET لیست مقالات
/api/articles/1/ GET جزئیات مقاله ۱

با ModelViewSet:

آدرس متد توضیح
/api/articles/ GET لیست مقالات
/api/articles/ POST ساخت مقاله جدید
/api/articles/1/ GET جزئیات مقاله ۱
/api/articles/1/ PUT بروزرسانی کامل
/api/articles/1/ PATCH بروزرسانی جزیی
/api/articles/1/ DELETE حذف
 

می‌بینید که ReadOnlyModelViewSet فقط دو مسیر GET دارد. بقیه مسیرها اصلاً ساخته نمی‌شوند.

تنظیم مسیر با Router

دقیقاً مثل ModelViewSet:

from rest_framework.routers import DefaultRouter
from django.urls import path, include

router = DefaultRouter()
router.register('articles', ArticleReadOnlyViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
]

Router خودکار تشخیص می‌دهد که این ViewSet فقط خواندنی است و فقط مسیرهای مربوط به list و retrieve را می‌سازد.

سفارشی‌سازی در ReadOnlyModelViewSet

مثل ModelViewSet، می‌توانید get_queryset و get_serializer_class را بازنویسی کنید.

فیلتر بر اساس کاربر:

def get_queryset(self):
    user = self.request.user
    if user.is_authenticated:
        return Article.objects.filter(is_published=True)
    return Article.objects.none()  # کاربر لاگین نکرده هیچ مقاله‌ای نمی‌بیند

تغییر Serializer برای عملیات‌های مختلف:

def get_serializer_class(self):
    if self.action == 'list':
        return ArticleListSerializer  # فقط فیلدهای خلاصه
    return ArticleDetailSerializer  # همه فیلدها برای جزئیات

self.action در اینجا می‌تواند 'list' یا 'retrieve' باشد.

مزایای استفاده از ReadOnlyModelViewSet

امنیت بیشتر

وقتی از ReadOnlyModelViewSet استفاده می‌کنید، هیچ خطری وجود ندارد که کاربر از طریق API داده را تغییر دهد. چون عملیات‌های نوشتنی اصلاً وجود ندارند.

سادگی Browsable API

صفحه Browsable API فقط دو عملیات GET را نشان می‌دهد. کاربر عادی گیج نمی‌شود.

کد کمتر و واضح‌تر

وقتی کد را می‌خوانید، بلافاصله متوجه می‌شوید که این API فقط خواندنی است. نیازی به بررسی تنظیمات دسترسی نیست.

محدودیت ReadOnlyModelViewSet

بزرگترین محدودیت: اگر بعداً نیاز به عملیات نوشتنی پیدا کردید، باید کلاس ویو را عوض کنید. از ReadOnlyModelViewSet به ModelViewSet تغییر دهید.

این تغییر ساده است. فقط کافی است خط زیر را عوض کنید:

# قبل
class ArticleViewSet(ReadOnlyModelViewSet):

# بعد
class ArticleViewSet(ModelViewSet):

بقیه کد (queryset، serializer_class، و تنظیمات Router) بدون تغییر می‌ماند.

تمرین این بخش

یک ReadOnlyModelViewSet برای مدل Product بسازید. سپس در Router ثبت کنید. در مرورگر تست کنید. آیا دکمه POST، PUT و DELETE در Browsable API نشان داده می‌شود؟

Router خودکار (DefaultRouter و SimpleRouter)

در روش‌های قبلی (APIView و Generic Views)، شما خودتان مسیرها را در urlpatterns تعریف می‌کردید. برای هر کلاس یک path می‌نوشتید.

با ViewSet، این کار تغییر می‌کند.

شما فقط به Router می‌گویید این ViewSet را ثبت کن. Router خودش تشخیص می‌دهد که ViewSet چه عملیاتی دارد و مسیرهای مربوطه را می‌سازد.

چرا به Router نیاز داریم؟

دلیل اول: حذف کدهای تکراری

برای یک ViewSet ساده، باید شش مسیر دستی بنویسید. با Router، این کار را نمی‌کنید.

دلیل دوم: ثبات و یکپارچگی

همه مسیرهای ViewSet شما از یک الگو پیروی می‌کنند. مثلاً همه آدرس‌های جزئیات به /{id}/ ختم می‌شوند.

دلیل سوم: نگهداری آسان

اگر تصمیم بگیرید آدرس پایه API را تغییر دهید (مثلاً از api/ به v1/api/)، فقط کافی است یک جا را تغییر دهید.

معرفی DefaultRouter

DefaultRouter رایج‌ترین Router در DRF است. همه چیز را دارد.

ویژگی‌های DefaultRouter:

  • همه مسیرهای استاندارد ViewSet را می‌سازد
  • یک صفحه ریشه (root) در آدرس / ایجاد می‌کند که لیست همه اندپوینت‌ها را نشان می‌دهد
  • قابلیت اتصال مسیرهای سفارشی با @action را دارد

کد مورد نیاز:

from rest_framework.routers import DefaultRouter
from django.urls import path, include
from .views import ArticleViewSet

router = DefaultRouter()
router.register('articles', ArticleViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
]

صفحه ریشه DefaultRouter:

وقتی به آدرس http://127.0.0.1:8000/api/ بروید، صفحه‌ای می‌بینید که همه ViewSetهای ثبت شده را لیست می‌کند:

{
    "articles": "http://127.0.0.1:8000/api/articles/"
}

این صفحه برای تست و کشف API خیلی مفید است.

معرفی SimpleRouter

SimpleRouter نسخه ساده‌تر DefaultRouter است. بیشتر ویژگی‌های مشابه را دارد، اما یک تفاوت بزرگ:

SimpleRouter صفحه ریشه (root view) ایجاد نمی‌کند.

اگر به آدرس /api/ بروید، صفحه ۴۰۴ می‌بینید. برای دیدن لیست مقالات باید مستقیم به /api/articles/ بروید.

کی از SimpleRouter استفاده کنیم؟

  • وقتی نمی‌خواهید صفحه ریشه در دسترس باشد
  • وقتی فقط یک یا دو ViewSet دارید و صفحه ریشه ضروری نیست
  • وقتی می‌خواهید کنترل بیشتری روی مسیر ریشه داشته باشید

کد SimpleRouter:

from rest_framework.routers import SimpleRouter

router = SimpleRouter()  # فقط همین خط فرق دارد
router.register('articles', ArticleViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
]

تفاوت DefaultRouter و SimpleRouter 

ویژگی DefaultRouter SimpleRouter
صفحه ریشه (root view) دارد ندارد
لیست خودکار اندپوینت‌ها دارد ندارد
مناسب برای توسعه بله خیر
مناسب برای تولید بله (با احتیاط) بله
حجم کد اضافی خیلی کم خیلی کم

مسیرهایی که Router می‌سازد

برای یک ModelViewSet به نام ArticleViewSet که با نام articles ثبت شده، Router مسیرهای زیر را می‌سازد:

آدرس متد عملیات
/api/articles/ GET list
/api/articles/ POST create
/api/articles/{id}/ GET retrieve
/api/articles/{id}/ PUT update
/api/articles/{id}/ PATCH partial_update
/api/articles/{id}/ DELETE destroy

توجه کنید که Router خودکار {id} را به آدرس اضافه می‌کند. نیازی نیست خودتان آن را بنویسید.

ثبت چند ViewSet در یک Router

router = DefaultRouter()
router.register('articles', ArticleViewSet)
router.register('users', UserViewSet)
router.register('products', ProductViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
]

نکات مهم در استفاده از Router

نکته اول: نام ثبت شده (basename)

router.register('articles', ArticleViewSet) - آرگومان اول (articles) نامی است که در آدرس دیده می‌شود. مثلاً /api/articles/.

اگر این نام را اشتباه بگذارید، Router خطا نمی‌دهد اما آدرس‌ها چیزی غیر از چیزی که انتظار دارید می‌شوند.

نکته دوم: تعیین basename در شرایط خاص

گاهی DRF نمی‌تواند خودکار basename را تشخیص دهد. مثلاً وقتی ViewSet از ViewSet (نه ModelViewSet) ارث‌بری می‌کند. در این موارد باید basename را مشخص کنید:

router.register('articles', ArticleViewSet, basename='article')

نکته سوم: اضافه کردن مسیرهای معمولی در کنار Router

اگر در کنار ViewSetها، ویوهای معمولی (مثل APIView) هم دارید، می‌توانید آن‌ها را به urlpatterns اضافه کنید:

urlpatterns = [
    path('api/', include(router.urls)),
    path('api/health/', HealthCheckView.as_view()),  # ویو معمولی
]

تمرین برای شما

یک پروژه با دو مدل Article و User بسازید. برای هر کدام یک ModelViewSet بنویسید. هر دو را در یک DefaultRouter ثبت کنید. آدرس ریشه (/api/) را در مرورگر باز کنید. چه می‌بینید؟

چطور URL خود را با ViewSet تنظیم کنیم (دو روش)

برای وصل کردن یک ViewSet به مسیرها، دو روش دارید. یکی ساده و خودکار، دیگری دستی و دقیق.

روش اول: استفاده از Router (روش استاندارد)

این روش ساده‌ترین و رایج‌ترین راه است.

from rest_framework.routers import DefaultRouter
from django.urls import path, include
from .views import ArticleViewSet

router = DefaultRouter()
router.register('articles', ArticleViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
]

مزیت این روش: همه مسیرها خودکار ساخته می‌شوند. نیاز به نوشتن تک تک pathها نیست.

خروجی این کد، شش مسیر زیر است:

  • GET /api/articles/
  • POST /api/articles/
  • GET /api/articles/{id}/
  • PUT /api/articles/{id}/
  • PATCH /api/articles/{id}/
  • DELETE /api/articles/{id}/

روش دوم: اتصال دستی متدها (بدون Router)

گاهی نمی‌خواهید از Router استفاده کنید. مثلاً می‌خواهید فقط یک یا دو متد از ViewSet را در معرض دید قرار دهید. یا می‌خواهید آدرس‌های کاملاً سفارشی داشته باشید.

در این روش، خودتان مشخص می‌کنید هر متد ViewSet به کدام آدرس متصل شود.

from django.urls import path
from .views import ArticleViewSet

article_list = ArticleViewSet.as_view({
    'get': 'list',
    'post': 'create'
})

article_detail = ArticleViewSet.as_view({
    'get': 'retrieve',
    'put': 'update',
    'patch': 'partial_update',
    'delete': 'destroy'
})

urlpatterns = [
    path('api/articles/', article_list, name='article-list'),
    path('api/articles/<int:pk>/', article_detail, name='article-detail'),
]

توضیح روش دوم (اتصال دستی)

متد as_view() در ViewSet یک دیکشنری می‌گیرد. کلیدهای این دیکشنری متدهای HTTP هستند (GET, POST, PUT, PATCH, DELETE). مقدارهای آن نام عملیات‌های ViewSet هستند (list, create, retrieve, update, partial_update, destroy).

ViewSet.as_view({
    'get': 'list',      # درخواست GET → متد list
    'post': 'create',   # درخواست POST → متد create
})

می‌توانید فقط یک زیرمجموعه از عملیات‌ها را هم متصل کنید:

# فقط لیست و جزئیات، بدون ساخت و بروزرسانی و حذف
read_only = ArticleViewSet.as_view({
    'get': 'list'
})

read_only_detail = ArticleViewSet.as_view({
    'get': 'retrieve'
})

urlpatterns = [
    path('api/articles/', read_only, name='article-list'),
    path('api/articles/<int:pk>/', read_only_detail, name='article-detail'),
]
ویژگی Router اتصال دستی
حجم کد کم (۴-۵ خط) متوسط (۱۰-۱۵ خط)
خودکار بودن مسیرها بله خیر
کنترل روی تک مسیرها کم زیاد
صفحه ریشه (root view) دارد (در DefaultRouter) ندارد
مناسب برای اکثر پروژه‌ها نیازهای خاص و سفارشی

چه موقع از کدام روش استفاده کنیم؟

Router را انتخاب کنید اگر:

  • می‌خواهید همه عملیات‌های CRUD را داشته باشید
  • می‌خواهید کد کوتاه و تمیز باشد
  • به صفحه ریشه خودکار نیاز دارید (در DefaultRouter)

اتصال دستی را انتخاب کنید اگر:

  • فقط به چند عملیات خاص نیاز دارید (مثلاً فقط لیست و جزئیات)
  • می‌خواهید آدرس‌های کاملاً متفاوت با پیش‌فرض داشته باشید
  • نمی‌خواهید صفحه ریشه در دسترس باشد
  • می‌خواهید کنترل کامل روی نام مسیرها (name) داشته باشید

روش ترکیبی

می‌توانید هر دو روش را در یک پروژه استفاده کنید. بعضی ViewSetها را با Router ثبت کنید. بعضی را دستی.

router = DefaultRouter()
router.register('articles', ArticleViewSet)  # با Router

urlpatterns = [
    path('api/', include(router.urls)),
    path('api/special/', SpecialViewSet.as_view({'get': 'list'})),  # دستی
]

عیب‌یابی رایج

خطا: 'ViewSet' object has no attribute 'list'
دلیل: در as_view دیکشنری اشتباه نوشته‌اید. کلیدها متد HTTP هستند، مقادیر نام متدهای ViewSet.

خطا: مسیر articles/{id}/ کار نمی‌کند
دلیل: در اتصال دستی، فراموش کرده‌اید پارامتر <int:pk> را به آدرس اضافه کنید.

تمرین این بخش

یک ViewSet بنویسید و آن را با روش دستی به مسیرها متصل کنید. فقط عملیات‌های list و create را در دسترس قرار دهید. در Browsable API تست کنید. آیا دکمه PUT و DELETE را می‌بینید؟

افزودن متدهای سفارشی با دکوریتور @action

ModelViewSet شش عملیات استاندارد را پوشش می‌دهد: لیست، ساخت، جزئیات، بروزرسانی، بروزرسانی جزیی، و حذف.

اما در دنیای واقعی، همیشه این شش عملیات کافی نیستند.

مثال: می‌خواهید یک مقاله را منتشر کنید (نه حذفش کنید). یا یک کاربر را فعال کنید. یا آمار بازدید یک محصول را بگیرید. اینها عملیات‌هایی هستند که در شش عملیات استاندارد وجود ندارند.

اینجا @action وارد می‌شود.

@action چیست؟

@action یک دکوریتور در DRF است. به شما اجازه می‌دهد هر متد دلخواهی را به ViewSet اضافه کنید. DRF این متد را به یک اندپوینت API تبدیل می‌کند.

اولین مثال: افزودن یک اکشن ساده

فرض کنید می‌خواهید یک اکشن به اسم latest اضافه کنید. این اکشن سه مقاله آخر را برمی‌گرداند.

from rest_framework.viewsets import ModelViewSet
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Article
from .serializers import ArticleSerializer

class ArticleViewSet(ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    
    @action(detail=False, methods=['get'])
    def latest(self, request):
        latest_articles = self.get_queryset().order_by('-id')[:3]
        serializer = self.get_serializer(latest_articles, many=True)
        return Response(serializer.data)

حالا یک آدرس جدید به ViewSet اضافه شده است:

GET /api/articles/latest/

پارامترهای مهم @action

detail=True یا detail=False

  • detail=False: اکشن روی کل مجموعه کار می‌کند. آدرس: /articles/latest/
  • detail=True: اکشن روی یک عضو خاص کار می‌کند. آدرس: /articles/1/publish/

methods=[]

لیست متدهای HTTP مجاز. می‌تواند ['get']، ['post']، ['put']، ['patch']، ['delete'] یا ترکیبی از آن‌ها باشد.

url_path

آدرس دلخواه برای اکشن. اگر ندهید، همان اسم متد استفاده می‌شود.

url_name

اسم مسیر برای استفاده در reverse().

مثال با detail=True

فرض کنید می‌خواهید یک اکشن برای انتشار مقاله داشته باشید. کاربر با ارسال درخواست POST به آدرس /articles/1/publish/، مقاله را منتشر می‌کند.

class ArticleViewSet(ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer
    
    @action(detail=True, methods=['post'])
    def publish(self, request, pk=None):
        article = self.get_object()
        article.is_published = True
        article.save()
        return Response({'status': 'article published'})

آدرس جدید:

POST /api/articles/1/publish/

اکشن با متدهای مختلف

می‌توانید یک اکشن را با چند متد HTTP مجاز کنید:

@action(detail=True, methods=['get', 'post'])
def stats(self, request, pk=None):
    article = self.get_object()
    if request.method == 'GET':
        # برگرداندن آمار
        return Response({'views': article.views})
    elif request.method == 'POST':
        # ثبت یک بازدید جدید
        article.views += 1
        article.save()
        return Response({'status': 'view recorded'})

تغییر آدرس اکشن با url_path

گاهی می‌خواهید آدرس اکشن با نام متد فرق کند:

@action(detail=True, methods=['post'], url_path='make-published')
def publish(self, request, pk=None):
    # ...

حالا آدرس به جای /publish/، /make-published/ می‌شود.

اکشن‌های سفارشی با Serializer متفاوت

می‌توانید در هر اکشن از Serializer متفاوتی استفاده کنید:

@action(detail=False, methods=['get'])
def latest(self, request):
    latest_articles = self.get_queryset().order_by('-id')[:3]
    serializer = ArticleSummarySerializer(latest_articles, many=True)
    return Response(serializer.data)

یا از get_serializer_class استفاده کنید:

@action(detail=True, methods=['post'])
def publish(self, request, pk=None):
    # ...
    return Response({'status': 'done'})

def get_serializer_class(self):
    if self.action == 'latest':
        return ArticleSummarySerializer
    return ArticleSerializer

اکشن‌هایی که داده ورودی می‌گیرند

اکشن با متد POST و دریافت داده از بدنه درخواست:

@action(detail=True, methods=['post'])
def rate(self, request, pk=None):
    article = self.get_object()
    rating = request.data.get('rating')
    if not rating or not 1 <= rating <= 5:
        return Response({'error': 'rating must be between 1 and 5'}, status=400)
    article.rating = rating
    article.save()
    return Response({'status': 'rated', 'rating': rating})

مسیرهایی که @action می‌سازد

برای detail=False:

متد آدرس
latest (GET) /articles/latest/

برای detail=True:

متد آدرس
publish (POST) /articles/{pk}/publish/
rate (POST) /articles/{pk}/rate/

نکات مهم در استفاده از @action

نکته اول: اکشن‌ها فقط در ViewSet کار می‌کنند. در APIView یا Generic Views معنایی ندارند.

نکته دوم: اگر از Router استفاده می‌کنید، نیازی به کار اضافه نیست. Router خودکار مسیرهای اکشن‌ها را تشخیص می‌دهد و اضافه می‌کند.

نکته سوم: اگر از روش اتصال دستی استفاده می‌کنید، اکشن‌ها به صورت خودکار در دسترس نیستند. باید خودتان آن‌ها را به as_view اضافه کنید.

نکته چهارم: در اکشن‌های detail=True، پارامتر pk به متد شما فرستاده می‌شود. از self.get_object() برای دریافت شیء استفاده کنید.

لیست اکشن‌های پرکاربرد

اکشن detail متد کاربرد
latest False GET آخرین رکوردها
search False GET جستجو
publish True POST انتشار یک آیتم
archive True POST آرشیو کردن
toggle_status True POST تغییر وضعیت
stats True GET آمار یک آیتم

تمرین برای شما

یک مدل Task با فیلدهای title، completed (بولین)، و created_at بسازید. سپس یک ViewSet برای آن بنویسید. دو اکشن اضافه کنید:

  • completed_list (detail=False) - لیست کارهای انجام شده را برگرداند
  • mark_complete (detail=True) - یک کار خاص را انجام شده کند

عیب‌یابی خطاهای رایج در ViewSet و Router

در این بخش، رایج‌ترین خطاهایی که هنگام کار با ViewSet و Router پیش می‌آید را بررسی می‌کنیم.

خطای اول: فراموشی ثبت ViewSet در Router

نشانه خطا:
آدرس‌های ViewSet شما کار نمی‌کنند. خطای ۴۰۴ می‌گیرید.

دلیل:
ViewSet را نوشته‌اید اما در Router ثبت نکرده‌اید.

کد اشتباه:

router = DefaultRouter()
# فراموش شده: router.register('articles', ArticleViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
]

کد درست:

router = DefaultRouter()
router.register('articles', ArticleViewSet)  # این خط لازم است

خطای دوم: خطای basename در Router

نشانه خطا:
basename argument not specified and could not be automatically determined

دلیل:
DRF نمی‌تواند خودکار basename را تشخیص دهد. این اتفاق معمولاً وقتی می‌افتد که ViewSet از ViewSet (نه ModelViewSet) ارث‌بری می‌کند یا queryset تعریف نشده است.

کد اشتباه:

class MyViewSet(viewsets.ViewSet):  # ViewSet ساده، نه ModelViewSet
    def list(self, request):
        return Response({'message': 'hello'})

router.register('my', MyViewSet)  # خطای basename

کد درست:

router.register('my', MyViewSet, basename='my')

خطای سوم: اشتباه در نام عملیات در as_view

نشانه خطا:
AttributeError: 'ViewSet' object has no attribute 'get_list'

دلیل:
در as_view دیکشنری را اشتباه نوشته‌اید. کلیدها متد HTTP هستند، مقادیر نام متدهای ViewSet.

کد اشتباه:

article_list = ArticleViewSet.as_view({
    'get': 'get_list',  # متد get_list وجود ندارد
})

ک درست:

article_list = ArticleViewSet.as_view({
    'get': 'list',  # نام درست متد، list است
})

خطای چهارم: فراموشی pk در آدرس هنگام اتصال دستی

نشانه خطا:
درخواست به articles/1/ با خطای ۴۰۴ مواجه می‌شود.

دلیل:
در اتصال دستی، فراموش کرده‌اید پارامتر <int:pk> را به آدرس اضافه کنید.

کد اشتباه:

urlpatterns = [
    path('api/articles/', article_list),
    path('api/articles/', article_detail),  # اشتباه: آدرس یکسان است
]

ک درست:

urlpatterns = [
    path('api/articles/', article_list),
    path('api/articles/<int:pk>/', article_detail),  # درست
]

خطای پنجم: فراموشی @action دکوریتور

نشانه خطا:
متد سفارشی شما در دسترس نیست. خطای ۴۰۴ می‌گیرید.

دلیل:
متد سفارشی را بدون @action نوشته‌اید.

کد اشتباه:

class ArticleViewSet(ModelViewSet):
    def latest(self, request):  # بدون @action
        ...

ک درست:

from rest_framework.decorators import action

class ArticleViewSet(ModelViewSet):
    @action(detail=False, methods=['get'])  # با @action
    def latest(self, request):
        ...

خطای ششم: فراموشی detail=True برای اکشن‌های روی یک عضو

نشانه خطا:
اکشنی که باید روی یک مقاله خاص اجرا شود (مثل /articles/1/publish/)، با خطای ۴۰۴ مواجه می‌شود یا آدرس اشتباه ساخته می‌شود.

دلیل:
detail=True را فراموش کرده‌اید.

کد اشتباه:

@action(detail=False, methods=['post'])  # detail=False
def publish(self, request, pk=None):
    ...

آدرس ساخته شده: /articles/publish/ (اشتباه)

کد درست:

@action(detail=True, methods=['post'])  # detail=True
def publish(self, request, pk=None):
    ...

آدرس ساخته شده: /articles/{pk}/publish/ (درست)

خطای هفتم: تعریف queryset یا serializer_class در ViewSet

نشانه خطا:
AttributeError: 'ArticleViewSet' object has no attribute 'queryset'

دلیل:
در ViewSet که از ModelViewSet ارث‌بری می‌کند، queryset و serializer_class را تعریف نکرده‌اید.

کد اشتباه:

class ArticleViewSet(ModelViewSet):
    # queryset و serializer_class تعریف نشده
    pass

کد درست:

class ArticleViewSet(ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

خطای هشتم: تداخل نام اکشن با نام مسیر

نشانه خطا:
اکشن شما کار می‌کند اما آدرس آن چیزی نیست که انتظار دارید.

دلیل:
نام متد با یک مسیر استاندارد تداخل دارد.

مثال: اگر یک اکشن با نام create بنویسید، با عملیات استاندارد create تداخل می‌کند.

راه حل: از نام‌های واضح و غیرتکراری استفاده کنید. مثلاً custom_create.

جدول جمع‌بندی خطاها و راه‌حل‌ها

خطا نشانه راه حل
فراموشی ثبت در Router ۴۰۴ برای همه مسیرها اضافه کردن router.register
خطای basename خطا در هنگام راه‌اندازی سرور اضافه کردن basename به register
اشتباه در as_view AttributeError بررسی دیکشنری as_view
فراموشی pk در آدرس ۴۰۴ برای جزئیات اضافه کردن <int:pk> به آدرس
فراموشی @action ۴۰۴ برای اکشن سفارشی اضافه کردن @action
detail اشتباه آدرس اشتباه اکشن تنظیم detail=True برای اکشن روی یک عضو
نداشتن queryset AttributeError اضافه کردن queryset و serializer_class

تمرین برای شما

کد زیر را بررسی کنید. سه خطا در آن وجود دارد. آن‌ها را پیدا کنید و اصلاح کنید.

from rest_framework.viewsets import ModelViewSet
from rest_framework.decorators import action

class ProductViewSet(ModelViewSet):
    serializer_class = ProductSerializer
    
    @action(detail=False)
    def available(self, request, pk=None):
        products = Product.objects.filter(stock__gt=0)
        serializer = self.get_serializer(products, many=True)
        return Response(serializer.data)

router = DefaultRouter()
router.register('products', ProductViewSet, basename='product')

مقایسه ViewSet + Router با روش‌های قبلی

حالا که با چهار روش مختلف برای نوشتن API در DRF آشنا شدید، وقت آن رسیده که آن‌ها را کنار هم بگذاریم و مقایسه کنیم.

چهار روش در یک نگاه

روش تعداد کلاس برای یک مدل حجم کد تقریبی تعریف مسیرها
Function-Based View متغیر (چند تابع) متوسط دستی
APIView ۲ کلاس زیاد (۵۰+ خط) دستی
Generic Views ۲ کلاس متوسط (۱۵ خط) دستی
ViewSet + Router ۱ کلاس کم (۱۰ خط) خودکار

مقایسه با مثال عملی

همه این روش‌ها یک API کامل CRUD برای مدل Article می‌سازند.

روش اول: Function-Based View (قبلی)

@api_view(['GET', 'POST'])
def article_list(request):
    if request.method == 'GET':
        articles = Article.objects.all()
        serializer = ArticleSerializer(articles, many=True)
        return Response(serializer.data)
    elif request.method == 'POST':
        serializer = ArticleSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=201)
        return Response(serializer.errors, status=400)

@api_view(['GET', 'PUT', 'DELETE'])
def article_detail(request, pk):
    try:
        article = Article.objects.get(pk=pk)
    except Article.DoesNotExist:
        return Response({'error': 'Not found'}, status=404)
    # منطق GET، PUT، DELETE...

حدود ۳۰-۴۰ خط کد. نیاز به دو تابع. مدیریت خطاهای ۴۰۴ دستی.

روش دوم: APIView 

class ArticleListCreateView(APIView):
    def get(self, request):
        articles = Article.objects.all()
        serializer = ArticleSerializer(articles, many=True)
        return Response(serializer.data)
    
    def post(self, request):
        serializer = ArticleSerializer(data=request.data)
        if serializer.is_valid():
            serializer.save()
            return Response(serializer.data, status=201)
        return Response(serializer.errors, status=400)

class ArticleDetailView(APIView):
    def get_object(self, pk):
        try:
            return Article.objects.get(pk=pk)
        except Article.DoesNotExist:
            return None
    
    def get(self, request, pk):
        article = self.get_object(pk)
        if not article:
            return Response({'error': 'Not found'}, status=404)
        # منطق PUT، DELETE...

حدود ۵۰-۶۰ خط کد. دو کلاس جداگانه. نیاز به متد کمکی get_object.

روش سوم: Generic Views 

class ArticleListCreateView(ListCreateAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

class ArticleDetailView(RetrieveUpdateDestroyAPIView):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

حدود ۱۰-۱۵ خط کد. دو کلاس جداگانه. دو مسیر دستی در urlpatterns.

روش چهارم: ViewSet + Router 

class ArticleViewSet(ModelViewSet):
    queryset = Article.objects.all()
    serializer_class = ArticleSerializer

# در urls.py
router = DefaultRouter()
router.register('articles', ArticleViewSet)
urlpatterns = [path('api/', include(router.urls))]

حدود ۱۰ خط کد. یک کلاس. مسیرها خودکار.

جدول مقایسه ویژگی‌ها

ویژگی FBV APIView Generic Views ViewSet + Router
یادگیری اولیه آسان متوسط متوسط سخت تر
حجم کد متوسط زیاد کم خیلی کم
کنترل روی جزئیات زیاد خیلی زیاد متوسط  کم
قابلیت استفاده مجدد کم متوسط زیاد خیلی زیاد
اضافه کردن متد سفارشی با تابع جدید با متد جدید با کلاس جدید با @action
مناسب برای پروژه کوچک بله بله بله شاید زیادی باشدبله
مناسب برای پروژه بزرگ خیر بله بله بله

چه موقع از کدام روش استفاده کنیم؟

Function-Based View:

  • پروژه خیلی کوچک با ۱-۲ API ساده
  • مرحله یادگیری و آشنایی با DRF
  • زمانی که به کنترل خیلی دقیق نیاز ندارید

APIView:

  • نیاز به کنترل کامل روی منطق هر متد
  • منطق غیراستاندارد و پیچیده
  • وقتی Generic Views و ViewSet نمی‌توانند نیاز شما را پوشش دهند

Generic Views:

  • پروژه متوسط با نیازهای استاندارد CRUD
  • زمانی که به ازای هر مدل، دو ویو جداگانه برایتان مشکل نیست
  • می‌خواهید تعادل بین سادگی و کنترل داشته باشید

ViewSet + Router:

  • پروژه بزرگ با چندین مدل
  • می‌خواهید کد خود را جمع‌وجور و قابل نگهداری کنید
  • به همه عملیات‌های CRUD نیاز دارید
  • می‌خواهید مسیرها خودکار ساخته شوند

مقایسه از نظر تعداد فایل‌ها

برای یک پروژه با ۵ مدل مختلف:

روش تعداد کلاس ویو تعداد مسیرهای دستی حجم کد تقریبی
APIView ۱۰ کلاس ۱۰ مسیر ۲۵۰+ خط
Generic Views ۱۰ کلاس ۱۰ مسیر ۱۰۰ خط
ViewSet + Router ۵ کلاس ۰ مسیر (خودکار) ۵۰ خط

مزایای ViewSet + Router نسبت به سایر روش‌ها

  • کد کمتر: یک کلاس به جای دو یا سه کلاس
  • مسیر خودکار: بدون نیاز به نوشتن urlpatterns
  • یکپارچگی: همه عملیات‌های یک مدل در یک جا
  • اکشن‌های سفارشی آسان: با @action بدون نیاز به کلاس جدید
  • نگهداری بهتر: تغییر در یک کلاس، نه چند کلاس جداگانه

معایب ViewSet + Router

  • منحنی یادگیری بالاتر: نسبت به روش‌های ساده‌تر
  • کنترل کمتر: پشت صحنه بیشتری نسبت به APIView دارد
  • ممکن است زیادی باشد: برای پروژه‌های خیلی ساده، استفاده از ViewSet زیادی به نظر می‌رسد
  • دیباگ سخت‌تر: وقتی خطایی رخ می‌دهد، ردگیری آن نسبت به FBV سخت‌تر است

توصیه نهایی

اگر تازه با DRF آشنا شده‌اید، از Function-Based View شروع کنید تا درک درستی از جریان درخواست و پاسخ پیدا کنید.

سپس به APIView بروید تا با ساختار کلاس‌بیس آشنا شوید.

بعد از آن Generic Views را یاد بگیرید تا کدهای تکراری را کاهش دهید.

و در نهایت، ViewSet و Router را برای پروژه‌های واقعی و حرفه‌ای استفاده کنید.

در پروژه‌های تجاری و شرکتی، ViewSet + Router استاندارد اصلی است. چون نگهداری و توسعه آن در بلندمدت بسیار آسان‌تر است.

جمع‌بندی و آماده شدن برای درس احراز هویت

در این درس، با قدرتمندترین روش نوشتن ویو در DRF آشنا شدید.

ViewSet را یاد گرفتید. کلاسی که تمام عملیات‌های CRUD را در یک جا جمع می‌کند. دیگر خبری از دو یا سه کلاس جداگانه برای هر مدل نیست.

ModelViewSet را دیدید. کامل‌ترین ویو DRF که شش عملیات استاندارد را پوشش می‌دهد.

ReadOnlyModelViewSet را مرور کردید. نسخه سبک‌تری که فقط برای نمایش داده طراحی شده.

با Router آشنا شدید. ابزاری که مسیرها را خودکار می‌سازد و شما را از نوشتن urlpatterns دستی بی‌نیاز می‌کند.

دو روش تنظیم URL را یاد گرفتید. روش استاندارد با Router و روش دستی برای مواقع خاص.

دکوریتور @action را فرا گرفتید. با آن می‌توانید هر متد سفارشی‌ای به ViewSet اضافه کنید. بدون نیاز به نوشتن کلاس جدید.

خطاهای رایج را بررسی کردید و یاد گرفتید هر کدام را چطور برطرف کنید.

و در انتها، ViewSet را با سه روش قبلی مقایسه کردید. دیدید هر کدام برای چه پروژه‌هایی مناسب است.

از این درس چه چیزی باید به خاطر بسپارید؟

  • برای پروژه‌های جدید و حرفه‌ای، ViewSet + Router را انتخاب کنید.
  • اگر فقط به عملیات خواندنی نیاز دارید، از ReadOnlyModelViewSet استفاده کنید.
  • برای اضافه کردن متدهای سفارشی مثل publish/ یا archive/، از @action استفاده کنید.
  • اگر پروژه شما خیلی ساده است، ViewSet زیادی به نظر می‌رسد. در آن موارد Generic Views یا حتی APIView کافی است.

درس بعدی چیست؟

تا الان، APIهایی ساختید که همه می‌توانند از آن‌ها استفاده کنند. هیچ محدودیتی ندارند. هر کسی می‌تواند مقاله بخواند، بسازد، بروزرسانی کند و حذف نماید.

در دنیای واقعی، اینطور نیست.

کاربر معمولی فقط باید مقاله بخواند. نویسنده باید بتواند مقاله بسازد و بروزرسانی کند. ادمین باید همه چیز را کنترل کند.

به این می‌گویند احراز هویت و مجوزها.

در درس بعدی، یاد می‌گیرید:

چطور کاربران را با Token Authentication شناسایی کنید

چطور مشخص کنید چه کسی اجازه چه کاری را دارد

چطور دسترسی‌های سطحی (مثلاً فقط نویسنده مقاله بتواند آن را ویرایش کند) پیاده‌سازی کنید

برای درس بعد، همین پروژه و مدل Article که الان دارید، کافی است. فقط یک فیلد author به مدل اضافه می‌کنیم تا مالکیت مقاله را مشخص کنیم.

آماده‌اید؟ بریم سراغ درس احراز هویت.