کلاس و شیء در جاوا؛ آموزش شیگرایی به زبان ساده
کلاس در جاوا نقشه است و شیء چیزی است که از روی آن نقشه ساخته میشود. در این آموزش ساده، …
با Django REST Framework میتوانید روی پروژه جنگو یک API تمیز و امن بسازید. در این آموزش قدمبهقدم، یک API کتاب با Serializer، ViewSet، Router، احراز هویت، مجوز سفارشی، صفحهبندی و آزمون خودکار میسازیم؛ همه کدها با جنگو ۶.۱.۳ و DRF ۳.۱۸.۳ اجرا و آزمایش شدهاند.
آخرین بهروزرسانی: ۱۸ آبان ۱۴۰۵ — منابع بررسیشده در این روز: صفحه دریافت جنگو، PyPI بسته DRF، صفحه اصلی و یادداشتهای انتشار DRF و راهنماهای Serializer، ViewSet، Router، احراز هویت، مجوز، صفحهبندی، آزمون و رابط مرورگری؛ کدها با Django 6.1.3، DRF 3.18.3 و پایتون ۳.۱۳.۵ اجرا شدند.
⚡ پاسخ کوتاه
DRF را نصب و به INSTALLED_APPS اضافه کنید. با ModelSerializer داده مدل را به JSON تبدیل و ورودی را اعتبارسنجی کنید، با ModelViewSet عملیات کامل یک منبع را بنویسید و با DefaultRouter آدرسها را خودکار بسازید. مجوز پیشفرض DRF دسترسی آزاد است؛ پس احراز هویت، مجوز و صفحهبندی را در تنظیمات آگاهانه تعیین کنید.
📑 فهرست مطالب
Django REST Framework یا به اختصار DRF یک کتابخانه برای جنگو است که ساختن API را ساده میکند. API راهی است که برنامههای دیگر، مثل اپلیکیشن موبایل، سایت ساختهشده با React یا سرویس یک شرکت دیگر، بتوانند با دادههای سایت شما کار کنند. معمولاً این گفتوگو با قالب JSON انجام میشود. اگر با مفهوم API هنوز آشنا نیستید، اتصال خدمات سایت با API را بخوانید. جنگو بهتنهایی هم میتواند JSON برگرداند. اما وقتی API جدی میسازید، کارهای تکراری زیادی پیش میآید: تبدیل شیءهای مدل به JSON و برعکس، بررسی درستی داده ورودی، احراز هویت، کنترل دسترسی، صفحهبندی و مستندسازی. DRF برای همه اینها ابزار آماده دارد. یعنی به جای اینکه هر بار این کارها را از صفر بنویسید، از قطعههای آزمودهشده استفاده میکنید. شاید بپرسید چرا FastAPI نه؟ FastAPI برای سرویسهای API مستقل گزینه خوبی است و DRF برای وقتی که پروژه جنگو دارید یا امکانات آماده جنگو، مثل پنل مدیریت و سیستم کاربران را میخواهید. مقایسه کامل را در جنگو یا FastAPI نوشتهایم و اینجا وارد آن بحث نمیشویم.
در روز نگارش این مقاله، صفحه دریافت جنگو آخرین نسخه رسمی را ۶.۱.۳ اعلام میکرد و آخرین نسخه DRF در PyPI، ۳.۱۸.۳ بود. مستندات DRF میگوید از جنگو ۵.۲، ۶.۰ و ۶.۱ و پایتون ۳.۱۰ تا ۳.۱۵ پشتیبانی میکند. خود جنگو ۶.۱ به پایتون ۳.۱۲ یا جدیدتر نیاز دارد. ما همه کدهای این آموزش را با جنگو ۶.۱.۳، DRF نسخه ۳.۱۸.۳ و پایتون ۳.۱۳ اجرا کردیم و آزمونهای انتهای مقاله هم با همین نسخهها پاس شدند. پیشنیاز این آموزش آشنایی پایه با جنگو است: ساختن پروژه و اپ، تعریف مدل و اجرای migration. اگر اینها برایتان تازه است، اول ساخت اپلیکیشن در جنگو و مدلها در جنگو را بخوانید. ساختار کلی پروژه را هم در ساختار MVT جنگو توضیح دادهایم.
اول یک محیط مجازی بسازید و جنگو و DRF را نصب کنید. نسخهها را صریح نوشتهایم تا نتیجه شما با این آموزش یکی باشد. در پروژه واقعی، نسخهها را در فایل requirements یا ابزار مدیریت وابستگی خودتان ثبت کنید.
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install Django==6.1.3 djangorestframework==3.18.3
django-admin startproject config .
python manage.py startapp books
بعد در فایل settings.py، دو اپ DRF و اپ خودمان را به INSTALLED_APPS اضافه میکنیم. rest_framework.authtoken را برای احراز هویت با توکن لازم داریم که جلوتر میبینیم. در همین فایل یک دیکشنری REST_FRAMEWORK هم میسازیم که تنظیمات کلی API را نگه میدارد.
INSTALLED_APPS = [
# ... django apps ...
"rest_framework",
"rest_framework.authtoken",
"books",
]
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": [
"rest_framework.authentication.SessionAuthentication",
"rest_framework.authentication.TokenAuthentication",
],
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticatedOrReadOnly",
],
"DEFAULT_PAGINATION_CLASS": "rest_framework.pagination.PageNumberPagination",
"PAGE_SIZE": 10,
}
برای اینکه مثالها ملموس باشند، یک API برای کتابها میسازیم. هر کتاب عنوان، نویسنده، قیمت، وضعیت انتشار و یک صاحب دارد. صاحب، کاربری است که کتاب را ثبت کرده و فقط خودش میتواند آن را ویرایش کند. توضیح کامل فیلدها و روابط را در مدلها در جنگو و نوشتن کوئریها را در آموزش Django ORM ببینید.
from django.conf import settings
from django.db import models
class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=120)
price = models.DecimalField(max_digits=10, decimal_places=2)
published = models.BooleanField(default=False)
owner = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="books",
)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at", "id"]
def __str__(self):
return self.title
بعد از نوشتن مدل، دستورهای makemigrations و migrate را اجرا کنید. ordering را عمداً تعریف کردهایم؛ صفحهبندی بدون ترتیب مشخص ممکن است در صفحههای مختلف نتیجه تکراری یا جاافتاده بدهد و جنگو هم در این حالت هشدار میدهد.

Serializer قلب DRF است. دو کار اصلی دارد: شیء مدل را به دادهای تبدیل میکند که بشود JSON کرد، و دادهای را که کاربر فرستاده، بررسی و به شیء مدل تبدیل میکند. یعنی هم خروجی را میسازد و هم نگهبان ورودی است. اگر بخواهیم با زبان ساده بگوییم، Serializer چیزی شبیه فرم جنگو است، اما برای API.
from rest_framework import serializers
from .models import Book
class BookSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source="owner.username")
class Meta:
model = Book
fields = ["id", "title", "author", "price", "published", "owner", "created_at"]
read_only_fields = ["created_at"]
def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError("Price must be greater than zero.")
return value
ModelSerializer فیلدها را از روی مدل میسازد؛ لازم نیست نوع هر فیلد را دوباره بنویسید. فهرست fields را همیشه صریح بنویسید. استفاده از "__all__" راحت است، اما اگر روزی فیلد حساسی به مدل اضافه شود، بیسروصدا در API هم ظاهر میشود. owner را فقطخواندنی کردهایم تا کاربر نتواند کتاب را به نام کس دیگری ثبت کند؛ مقدارش را در ویو از کاربر فعلی میگیریم.
متدی که با validate_ و نام فیلد شروع شود، فقط همان فیلد را بررسی میکند. برای قانونهایی که به چند فیلد با هم مربوطاند، متد validate را بنویسید که کل داده را میگیرد. اگر داده نامعتبر باشد، DRF خودش پاسخ 400 با پیام خطا برای هر فیلد برمیگرداند. ما این رفتار را در آزمونها امتحان کردیم: ارسال قیمت صفر، پاسخ 400 با کلید price داد.
وقتی داده مستقیم به یک مدل مربوط است، ModelSerializer کار را کوتاه میکند. وقتی ورودی شکل خاصی دارد که به یک مدل نمیخورد، مثلاً فرم تماس یا درخواستی که چند مدل را با هم تغییر میدهد، از Serializer معمولی استفاده کنید و فیلدها و متدهای create و update را خودتان تعریف کنید.
DRF چند سطح ویو دارد. APIView پایهایترین است و کنترل کامل میدهد؛ متدهای get و post و بقیه را خودتان مینویسید. ویوهای عمومی مثل ListCreateAPIView کارهای رایج فهرست و ایجاد را آماده دارند. ViewSet یک قدم جلوتر است و همه عملیات یک منبع، یعنی فهرست، جزئیات، ایجاد، ویرایش و حذف را در یک کلاس جمع میکند. اگر با ویوهای خود جنگو و تفاوت ویوی تابعی و کلاسی آشنا نیستید، ویوها و URLها در جنگو را ببینید.
from rest_framework import permissions, viewsets
from .models import Book
from .permissions import IsOwnerOrReadOnly
from .serializers import BookSerializer
class BookViewSet(viewsets.ModelViewSet):
serializer_class = BookSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly]
def get_queryset(self):
qs = Book.objects.select_related("owner")
if self.request.query_params.get("published") == "true":
qs = qs.filter(published=True)
return qs
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
چند نکته در این کلاس هست. get_queryset را به جای یک queryset ثابت نوشتهایم تا بتوانیم بر اساس پارامتر آدرس فیلتر کنیم. select_related باعث میشود نام صاحب هر کتاب با همان کوئری اصلی خوانده شود و برای هر کتاب کوئری جدا زده نشود؛ این همان مشکل N+1 است که در آموزش Django ORM توضیح دادهایم. perform_create هم جایی است که صاحب کتاب را از کاربر واردشده میگیریم.
ModelViewSet برای منابع ساده و استاندارد عالی است. اما اگر یک endpoint فقط یک کار خاص انجام میدهد، مثل «ارسال دوباره ایمیل تأیید»، یا منطقش با الگوی فهرست و جزئیات جور نیست، APIView یا یک ویوی عمومی سادهتر و خواناتر است. لازم نیست همه چیز ViewSet باشد. برای افزودن یک عمل خاص به ViewSet هم DRF دکوراتور action را دارد.
وقتی از ViewSet استفاده میکنید، لازم نیست آدرسها را یکییکی بنویسید. Router این کار را انجام میدهد. با ثبت BookViewSet، آدرس فهرست و ایجاد در books/ و آدرس جزئیات، ویرایش و حذف در books/ بهاضافه شناسه کتاب ساخته میشود. DefaultRouter یک صفحه ریشه هم برای API میسازد که فهرست منابع را نشان میدهد.
# books/urls.py
from django.urls import include, path
from rest_framework.routers import DefaultRouter
from .views import BookViewSet
router = DefaultRouter()
router.register("books", BookViewSet, basename="book")
urlpatterns = [
path("", include(router.urls)),
]
# config/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("api/", include("books.urls")),
path("api-auth/", include("rest_framework.urls")),
]
چون در ViewSet به جای queryset از get_queryset استفاده کردهایم، باید basename را صریح بدهیم؛ router از روی آن نام مسیرها را میسازد. مسیر api-auth هم صفحه ورود و خروج را برای رابط مرورگری DRF اضافه میکند.
یکی از امکانات دوستداشتنی DRF رابط مرورگری یا Browsable API است. وقتی آدرس api/books/ را در مرورگر باز کنید، به جای JSON خام، صفحهای میبینید که دادهها را نمایش میدهد و فرمی برای ارسال داده تازه دارد. این صفحه برای یادگیری و آزمایش سریع خیلی مفید است. با پیوند ورود بالای صفحه هم میتوانید وارد شوید و درخواستهایی را که به ورود نیاز دارند امتحان کنید. این رابط جای آزمون خودکار را نمیگیرد و در محیط عمومی هم بهتر است تصمیم آگاهانه بگیرید که فعال بماند یا فقط JSON برگردانید. این کار با تنظیم کلاسهای رندر در DRF انجام میشود.

این دو مفهوم را با هم اشتباه نگیرید. احراز هویت یعنی «تو کی هستی؟» و مجوز دسترسی یعنی «اجازه داری این کار را بکنی؟». DRF اول با کلاسهای احراز هویت کاربر را تشخیص میدهد و بعد با کلاسهای مجوز تصمیم میگیرد درخواست انجام شود یا نه. در تنظیمات بالا دو روش احراز هویت فعال است. SessionAuthentication همان ورود معمول جنگو با کوکی است و برای رابط مرورگری و سایتی که روی همان دامنه است مناسب است. TokenAuthentication برای برنامههایی مثل اپ موبایل است که با هر درخواست یک توکن در هدر Authorization میفرستند. توکن هر کاربر را میتوانید از پنل مدیریت یا با دستور drf_create_token بسازید. DRF بهطور پیشفرض JWT ندارد؛ اگر به آن نیاز دارید، باید از بستههای جانبی استفاده کنید و آنها را جداگانه بررسی کنید. مجوز IsAuthenticatedOrReadOnly یعنی همه میتوانند فهرست و جزئیات را ببینند، اما فقط کاربر واردشده میتواند ایجاد یا ویرایش کند. توجه کنید که اگر DEFAULT_PERMISSION_CLASSES را تنظیم نکنید، مقدار پیشفرض DRF، AllowAny است؛ یعنی دسترسی بدون محدودیت. پس همیشه این تنظیم را آگاهانه انتخاب کنید.
IsAuthenticatedOrReadOnly جلوی کاربر ناشناس را میگیرد، اما هر کاربر واردشده هنوز میتواند کتاب دیگران را ویرایش کند. برای این قانون یک مجوز سفارشی مینویسیم. متد has_object_permission روی هر شیء جداگانه اجرا میشود؛ درخواستهای فقطخواندنی مجازند و بقیه فقط برای صاحب کتاب.
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user
یک نکته امنیتی: مجوزهای سطح شیء فقط وقتی اجرا میشوند که ویو شیء را با get_object بگیرد. ViewSet و ویوهای عمومی این کار را انجام میدهند، اما اگر خودتان ویوی سفارشی مینویسید، حواستان باشد. همچنین این مجوزها فهرست را فیلتر نمیکنند؛ اگر کاربر نباید دادههای دیگران را حتی ببیند، باید در get_queryset فیلتر کنید.
اگر جدول کتابها هزاران ردیف داشته باشد، برگرداندن همه در یک پاسخ هم سرور را خسته میکند و هم برنامه کاربر را. در تنظیمات، PageNumberPagination با اندازه صفحه ۱۰ را فعال کردیم. حالا پاسخ فهرست، به جای یک لیست ساده، شامل count، آدرس صفحه بعد و قبل و results است. صفحه دوم را با پارامتر page=2 میگیرید. مستندات DRF تأکید میکند که هم کلاس صفحهبندی و هم PAGE_SIZE باید تنظیم شوند؛ هر دو بهطور پیشفرض خالیاند. اگر برنامه سمت کاربر شما با جابهجایی آیتمها در هنگام اضافه شدن داده تازه مشکل دارد، روشهای دیگری مثل LimitOffsetPagination یا CursorPagination را هم در مستندات ببینید. اندازه صفحه ده فقط یک انتخاب آموزشی است.
حالا که همه قطعهها را ساختیم، یک بار مسیر کامل یک درخواست را دنبال کنیم تا تصویر ذهنی روشن شود. فرض کنید اپ موبایل میخواهد کتاب تازهای ثبت کند و یک درخواست POST به آدرس api/books/ میفرستد. اول، سیستم آدرسدهی جنگو درخواست را به مسیری میفرستد که Router برای BookViewSet ساخته است. Router میداند که POST روی آدرس فهرست یعنی «ایجاد»، پس متد create در ViewSet صدا زده میشود. دوم، پیش از هر کاری، کلاسهای احراز هویت بررسی میکنند که درخواستدهنده کیست؛ مثلاً از روی توکنی که در هدر فرستاده شده. سوم، کلاسهای مجوز تصمیم میگیرند این کاربر اجازه ایجاد دارد یا نه. اگر کاربر ناشناس باشد، همینجا پاسخ خطا برمیگردد و هیچ دادهای به پایگاه داده نمیرسد. چهارم، داده ورودی به Serializer داده میشود. Serializer نوع فیلدها را بررسی میکند، متد validate_price را اجرا میکند و اگر مشکلی باشد، پاسخ ۴۰۰ با فهرست خطاها برمیگرداند. پنجم، اگر همهچیز درست بود، perform_create صاحب کتاب را از کاربر فعلی تعیین میکند و شیء در پایگاه داده ذخیره میشود. در پایان، همان Serializer شیء ذخیرهشده را به JSON تبدیل میکند و پاسخ ۲۰۱ به اپ برمیگردد. دانستن این ترتیب در عیبیابی خیلی کمک میکند. اگر پاسخ ۴۰۱ یا ۴۰۳ گرفتید، مشکل در احراز هویت یا مجوز است و کاری به Serializer ندارد. اگر پاسخ ۴۰۰ گرفتید، داده ورودی ایراد دارد و متن خطا دقیقاً میگوید کدام فیلد. اگر هم پاسخ درست است اما کند، سراغ کوئریها بروید.
API بدون آزمون خودکار، دیر یا زود با یک تغییر کوچک میشکند. DRF کلاس APITestCase و کلاینت آزمون خودش را دارد. در آزمونهای زیر رفتارهای اصلی را بررسی میکنیم: کاربر ناشناس فهرست را میبیند اما نمیتواند کتاب بسازد، صاحب کتاب خودکار تنظیم میشود، قیمت نامعتبر رد میشود و کاربر دیگر نمیتواند کتاب کسی را ویرایش کند.
from django.contrib.auth import get_user_model
from rest_framework import status
from rest_framework.test import APITestCase
from .models import Book
class BookApiTests(APITestCase):
def setUp(self):
User = get_user_model()
self.ali = User.objects.create_user("ali", password="test-pass-123")
self.sara = User.objects.create_user("sara", password="test-pass-123")
self.book = Book.objects.create(
title="Django Basics", author="A. Writer", price="120.00",
published=True, owner=self.ali,
)
def test_anonymous_can_list(self):
response = self.client.get("/api/books/")
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["count"], 1)
def test_anonymous_cannot_create(self):
response = self.client.post("/api/books/", {"title": "X", "author": "Y", "price": "10.00"})
self.assertIn(response.status_code, (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN))
def test_owner_is_set_from_request(self):
self.client.force_authenticate(user=self.sara)
response = self.client.post("/api/books/", {"title": "DRF", "author": "B", "price": "90.00"})
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["owner"], "sara")
def test_price_must_be_positive(self):
self.client.force_authenticate(user=self.sara)
response = self.client.post("/api/books/", {"title": "Bad", "author": "B", "price": "0"})
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
self.assertIn("price", response.data)
def test_only_owner_can_update(self):
self.client.force_authenticate(user=self.sara)
response = self.client.patch(f"/api/books/{self.book.id}/", {"price": "1.00"})
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
ما این پنج آزمون را با دستور python manage.py test books اجرا کردیم و هر پنج پاس شدند. برای کاربر ناشناس هر دو کد 401 و 403 را پذیرفتهایم، چون DRF بسته به کلاس احراز هویتی که اول در فهرست آمده، یکی از این دو را برمیگرداند. force_authenticate هم در آزمون، ورود کاربر را بدون نیاز به رمز و توکن شبیهسازی میکند.

اول، رها کردن مجوز پیشفرض. اگر DEFAULT_PERMISSION_CLASSES را تنظیم نکنید، API برای همه باز است. دوم، استفاده از fields = "__all__" که فیلدهای حساس را بیخبر بیرون میدهد. سوم، گرفتن صاحب یا کاربر از داده ورودی به جای request.user؛ این یعنی هر کسی میتواند ادعا کند کس دیگری است. چهارم، فراموش کردن صفحهبندی برای فهرستهای بزرگ. پنجم، نادیده گرفتن کوئریهای تکراری در Serializerهای تودرتو که با select_related و prefetch_related حل میشوند. در محیط واقعی چند کار دیگر هم لازم است: HTTPS را برای همه درخواستها اجباری کنید، چون توکن در هدر فرستاده میشود؛ برای endpointهای حساس محدودیت تعداد درخواست یا throttling تعریف کنید؛ DEBUG را در سرور اصلی خاموش کنید؛ و پیامهای خطا را طوری تنظیم کنید که اطلاعات داخلی سیستم را لو ندهند. هیچکدام از اینها جای بازبینی امنیتی متناسب با پروژه شما را نمیگیرد.
بعد از این آموزش، چند موضوع ارزش یادگیری دارد: فیلتر و جستوجوی پیشرفتهتر، نسخهبندی API، تولید مستندات و schema برای API، و throttling. مستندات رسمی DRF برای هر کدام بخش جداگانه دارد. اگر برای مصاحبه آماده میشوید، سؤالات مصاحبه جنگو را هم مرور کنید. برای کوئرینویسی بهینه پشت API هم دوباره به آموزش Django ORM سر بزنید، چون سرعت بیشتر APIها به کوئریهایشان بستگی دارد، نه به خود DRF.
نه. با JsonResponse و ویوهای خود جنگو هم میتوانید API ساده بسازید. اما برای API واقعی با اعتبارسنجی، احراز هویت، مجوز و صفحهبندی، DRF حجم زیادی از کار تکراری را حذف میکند و الگوهایش برای بیشتر توسعهدهندگان جنگو آشناست.
APIView کلاس پایه است و هر متد HTTP را خودتان مینویسید. ViewSet عملیات معمول یک منبع را در یک کلاس جمع میکند و با Router، آدرسها خودکار ساخته میشوند. برای منابع استاندارد ViewSet کد را کوتاه میکند و برای endpointهای خاص، APIView روشنتر است.
DRF بهطور پیشفرض احراز هویت پایه، نشست و توکن ساده را دارد. برای JWT باید از بستههای جانبی استفاده کنید. پیش از انتخاب، وضعیت نگهداری بسته و سازگاری آن با نسخه DRF و جنگوی خودتان را بررسی کنید.
ما برای این مقاله آزمون سرعت اجرا نکردهایم و عددی ادعا نمیکنیم. در بیشتر APIها، زمان پاسخ بیشتر به کوئریهای پایگاه داده و حجم داده برمیگردد. قبل از نتیجهگیری، با ابزار پروفایل و داده واقعی اندازه بگیرید و اول مشکلهای رایج مثل N+1 را برطرف کنید.
بله. طبق مستندات رسمی DRF در روز نگارش، نسخه ۳.۱۸ از جنگو ۵.۲، ۶.۰ و ۶.۱ پشتیبانی میکند و ما هم مثالهای این مقاله را با جنگو ۶.۱.۳ و DRF ۳.۱۸.۳ اجرا کردهایم.
نظرات 0
برای ارسال نظر وارد حساب کاربری شوید.
هنوز نظری ثبت نشده؛ اولین نفر باشید.