ویرگول
ورودثبت نام
محمدصالح عظیمی دخت
محمدصالح عظیمی دختSaleh Azimidokht توسعه‌دهنده Backend با تمرکز بر Python، Django و طراحی سیستم‌های مقیاس‌پذیر و امن. https://cyberhuginn.com
محمدصالح عظیمی دخت
محمدصالح عظیمی دخت
خواندن ۶ دقیقه·۲ ماه پیش

آیا سریالایزر ها در جنگو باعث کندی در اجرا درخواست می‌شوند؟

پنهان سریالایزر‌ها در جنگو
پنهان سریالایزر‌ها در جنگو

تا حالا برایتان پیش آمده که یک API در Django داشته باشید که Query آن خیلی سریع اجرا می‌شود، اما خود API همچنان کند باشد؟

PostgreSQL مشکلی ندارد.

Queryها سریع هستند.

View هم ظاهراً کار خاصی انجام نمی‌دهد.

اما Response همچنان چند صد میلی‌ثانیه یا حتی چند ثانیه طول می‌کشد.

یکی از جاهایی که معمولاً در بررسی Performance فراموش می‌شود، Serializer است.

در Django REST Framework، Serializer فقط وظیفه تبدیل Object به JSON را ندارد. ممکن است هنگام Serialization، Query جدید اجرا شود، Relationshipها خوانده شوند، محاسبات Python انجام شوند و حتی Serializerهای تو در تو باعث اجرای Queryهای بیشتری شوند.

و اینجاست که مشکلات Performance شروع می‌شوند.

Serializer همیشه فقط یک Formatter نیست

معمولاً معماری را این‌طور تصور می‌کنیم:

Database → Serializer → JSON Response

پس طبیعی است که فکر کنیم Serializer هزینه زیادی ندارد.

اما فرض کنید چنین Serializerای داریم:

class VehicleSerializer(serializers.ModelSerializer): customer_name = serializers.SerializerMethodField() class Meta: model = Vehicle fields = ( "id", "plate_number", "customer_name", ) def get_customer_name(self, obj): return obj.customer.name

کد کاملاً ساده و منطقی به نظر می‌رسد.

اما یک سؤال مهم وجود دارد:

obj.customer از قبل Load شده است؟

اگر نه، Django ممکن است برای دسترسی به customer یک Query جدید اجرا کند.

حالا اگر ۱۰۰ خودرو داشته باشیم:

1 Query → دریافت خودروها 100 Query → دریافت Customerها مجموع: 101 Query

این همان مشکل معروف N+1 Query است.

مشکل اینجاست که در ظاهر، هیچ Queryای داخل Serializer نوشته نشده است.

اما Serializer باعث اجرای آن شده است.


SerializerMethodField می‌تواند گران تمام شود

SerializerMethodField یکی از قابلیت‌های بسیار کاربردی Django REST Framework است.

اما استفاده نادرست از آن می‌تواند Performance API را به‌شدت کاهش دهد.

مثلاً:

class ServiceSerializer(serializers.ModelSerializer): last_service = serializers.SerializerMethodField() def get_last_service(self, obj): return ( Service.objects .filter( ownership=obj.ownership, ) .order_by("-created_at") .first() )

این کد برای هر Object یک Query اجرا می‌کند.

اگر ۵۰۰ Service را Serialize کنیم:

1 Query + 500 Query = 501 Query

کد درست کار می‌کند.

اما درست کار کردن با Performance خوب یکسان نیست.


راه‌حل همیشه تغییر Serializer نیست

یکی از مهم‌ترین نکات در بهینه‌سازی Django API این است:

Serializer نباید داده‌ای را کشف کند که QuerySet می‌توانسته از قبل آماده کند.

به جای اینکه برای هر Object یک Query جداگانه اجرا کنیم، می‌توانیم داده را در QuerySet آماده کنیم.

برای مثال با استفاده از Subquery:

from django.db.models import OuterRef, Subquery last_service = Service.objects.filter( ownership=OuterRef("ownership"), ).order_by("-created_at") queryset = Vehicle.objects.annotate( last_service_id=Subquery( last_service.values("id")[:1] ) )

حالا دیتابیس می‌تواند این اطلاعات را در همان Query اصلی آماده کند.

در نتیجه Serializer ساده‌تر می‌شود:

class VehicleSerializer(serializers.ModelSerializer): last_service_id = serializers.IntegerField( read_only=True, ) class Meta: model = Vehicle fields = ( "id", "plate_number", "last_service_id", )

یعنی به جای:

Database ↓ Serializer ↓ Query ↓ Query ↓ Query

به چیزی شبیه این می‌رسیم:

Database ↓ Optimized QuerySet ↓ Annotation ↓ Serializer ↓ JSON

تفاوت select_related و prefetch_related

اگر با Django کار می‌کنید، احتمالاً بارها با این دو متد روبه‌رو شده‌اید:

select_related() prefetch_related()

اما استفاده درست از آنها اهمیت زیادی دارد.

برای Relationshipهایی مثل:

  • ForeignKey

  • OneToOneField

معمولاً از select_related() استفاده می‌کنیم.

مثلاً:

vehicles = Vehicle.objects.select_related( "customer", )

در این حالت Django می‌تواند اطلاعات Customer را با استفاده از SQL JOIN همراه با Vehicle دریافت کند.

اما برای Relationshipهایی مثل:

  • ManyToManyField

  • Reverse ForeignKey

معمولاً prefetch_related() مناسب‌تر است.

مثلاً:

services = Service.objects.prefetch_related( "products", )

یک قانون ساده برای به خاطر سپردن:

ForeignKey / OneToOne ↓ select_related() ManyToMany / Reverse FK ↓ prefetch_related()

Serializerهای تو در تو؛ جایی که مشکل جدی‌تر می‌شود

Nested Serializerها بسیار کاربردی هستند.

مثلاً:

class ProductSerializer(serializers.ModelSerializer): class Meta: model = Product fields = ( "id", "name", ) class ServiceSerializer(serializers.ModelSerializer): products = ProductSerializer( many=True, ) class Meta: model = Service fields = ( "id", "products", )

در نگاه اول مشکلی وجود ندارد.

اما تصور کنید API شما ۱۰۰۰ Service برمی‌گرداند.

هر Service تعدادی Product دارد.

اگر Relationshipها به درستی Prefetch نشده باشند، تعداد Queryها می‌تواند به‌شدت افزایش پیدا کند.

و اگر این ساختار عمیق‌تر شود:

Service ├── Customer │ └── Address └── Products └── Category

مشکل پیچیده‌تر می‌شود.

یک JSON ساده ممکن است پشت صحنه تعداد زیادی Query ایجاد کرده باشد.


یک Serializer برای همه چیز نسازید

یکی دیگر از اشتباهات رایج این است که یک Serializer را برای تمام Endpointها استفاده کنیم.

مثلاً:

VehicleSerializer

هم برای:

List Detail Create Update Search Admin

استفاده شود.

با گذشت زمان Serializer بزرگ و پیچیده می‌شود.

بهتر است بر اساس نیاز Endpoint، Serializerهای متفاوت داشته باشیم.

مثلاً برای List:

class VehicleListSerializer(serializers.ModelSerializer): class Meta: model = Vehicle fields = ( "id", "plate_number", "color", )

اما برای Detail می‌توانیم اطلاعات بیشتری برگردانیم:

class VehicleDetailSerializer(serializers.ModelSerializer): customer = CustomerSerializer() class Meta: model = Vehicle fields = ( "id", "plate_number", "color", "customer", )

این کار فقط برای تمیزتر شدن Code نیست.

می‌تواند مستقیماً روی Performance API تأثیر بگذارد.


همه چیز را به Client برنگردانید

یکی از اشتباهات دیگر، برگرداندن حجم زیادی از اطلاعات در هر Response است.

مثلاً:

{ "id": 1, "customer": { "id": 10, "name": "...", "phone": "...", "vehicles": [], "services": [], "invoices": [], "payments": [] } }

ممکن است چنین Responseای از نظر Frontend جذاب باشد.

اما هر Relationship هزینه دارد.

هزینه می‌تواند شامل این موارد باشد:

  • Queryهای بیشتر

  • Serialization بیشتر

  • مصرف Memory بیشتر

  • Response بزرگ‌تر

  • انتقال داده بیشتر

  • پردازش بیشتر در Frontend

یک API خوب قرار نیست همه چیزهایی که Database می‌داند را برگرداند.

باید چیزی را برگرداند که Client واقعاً نیاز دارد.


قبل از بهینه‌سازی، اندازه‌گیری کنید

یکی از بدترین اشتباهات در Performance Optimization این است که بر اساس حدس عمل کنیم.

مثلاً:

«فکر کنم Serializer کند است.»

شاید.

اما باید اندازه‌گیری کنیم.

برای بررسی Performance یک API، مواردی مثل این‌ها مهم هستند:

تعداد Queryها مدت اجرای Queryها زمان Serialization زمان کل Request حجم Response مصرف CPU و Memory

ابزارهایی مثل Django Debug Toolbar و Django Silk می‌توانند برای پیدا کردن Queryهای تکراری و Requestهای کند بسیار مفید باشند.

چون همیشه Database مشکل اصلی نیست.

مثلاً:

Database: 40 ms View Logic: 30 ms Serialization: 650 ms JSON Rendering: 80 ms Total: 800 ms

در این شرایط اگر PostgreSQL را از ۴۰ میلی‌ثانیه به ۳۰ میلی‌ثانیه برسانیم، هنوز مشکل اصلی باقی است.

مشکل اصلی Serialization است.


چک‌لیست بهینه‌سازی Django REST Framework

وقتی یک API در Django REST Framework کند می‌شود، این موارد را بررسی کنید:

۱. تعداد Queryها را بررسی کنید

چند Query برای تولید Response اجرا می‌شود؟

۲. SerializerMethodFieldها را بررسی کنید

آیا داخل get_<field_name>() دیتابیس Query می‌شود؟

۳. Relationshipها را بررسی کنید

آیا کدی مثل این دارید؟

obj.customer obj.owner obj.category obj.products.all()

۴. select_related را بررسی کنید

برای ForeignKey و OneToOneField آیا Relationshipها به شکل مناسب Load شده‌اند؟

۵. prefetch_related را بررسی کنید

برای ManyToMany و Reverse Relationshipها چطور؟

۶. Annotationها را بررسی کنید

آیا می‌توان محاسبه‌ای را به جای Python در Database انجام داد؟

۷. Nested Data را کاهش دهید

آیا Frontend واقعاً به تمام این اطلاعات نیاز دارد؟

۸. Serializerهای جداگانه داشته باشید

آیا List Endpoint واقعاً به همان اطلاعات Detail Endpoint نیاز دارد؟

۹. دوباره اندازه‌گیری کنید

بعد از تغییرات، دوباره Performance را اندازه بگیرید.

Before ↓ Optimize ↓ Measure Again

نکته اصلی

Serializerهای Django REST Framework ذاتاً کند نیستند.

مشکل معمولاً نحوه استفاده ما از آنهاست.

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

  • بارها Query اجرا کند.

  • به Relationshipهای Load نشده دسترسی پیدا کند.

  • محاسبات سنگین Python انجام دهد.

  • Nested Serializerهای زیادی داشته باشد.

  • اطلاعات غیرضروری برگرداند.

  • دسترسی به Database را داخل Methodها پنهان کند.

در بسیاری از مواقع، بهترین راه‌حل این نیست که Serializer را تغییر دهیم.

بلکه باید QuerySet پشت Serializer را بهینه کنیم.

معماری ایده‌آل‌تر چیزی شبیه این است:

Request ↓ View ↓ Optimized QuerySet ↓ Database ↓ Serializer ↓ Minimal JSON Response

نه اینکه:

Request ↓ View ↓ Huge QuerySet ↓ Serializer ↓ Query ↓ Query ↓ Query ↓ N+1 ↓ Slow API

جمع‌بندی

دفعه بعد که یک API در Django REST Framework کند شد، فقط SQL را بررسی نکنید.

چیزی که بعد از اجرای QuerySet اتفاق می‌افتد را هم بررسی کنید.

ممکن است Query دیتابیس شما فقط ۵۰ میلی‌ثانیه زمان ببرد، اما Serializer چند صد میلی‌ثانیه دیگر به زمان Response اضافه کند.

پس اگر با Django و Django REST Framework کار می‌کنید، همیشه این سه مورد را کنار هم ببینید:

QuerySet + Database + Serializer

بهینه‌سازی واقعی زمانی اتفاق می‌افتد که هر سه را با هم بررسی کنید.


من نسخه کامل‌تر و فنی‌تر این موضوع، همراه با مثال‌های بیشتر درباره select_related، prefetch_related، Subquery، SerializerMethodField و N+1 Query را در سایت شخصی‌ام منتشر کرده‌ام!

اگر با Django API کار می‌کنید، پیشنهاد می‌کنم قبل از اینکه یک Serializer بزرگ‌تر بنویسید، اول ببینید دقیقاً چه چیزی پشت آن در حال Query شدن است.

۴
۰
محمدصالح عظیمی دخت
محمدصالح عظیمی دخت
Saleh Azimidokht توسعه‌دهنده Backend با تمرکز بر Python، Django و طراحی سیستم‌های مقیاس‌پذیر و امن. https://cyberhuginn.com
شاید از این پست‌ها خوشتان بیاید