تا الان سه روش برای نوشتن ویو در 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 به مدل اضافه میکنیم تا مالکیت مقاله را مشخص کنیم.
آمادهاید؟ بریم سراغ درس احراز هویت.