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

نتیجه؟ کندی سرور، مصرف زیاد پهنای باند، و用户体验 بد.

راه حل ساده است: همه داده‌ها را یکجا نفرست. تکه تکه بفرست.

به این می‌گویند صفحه‌بندی.

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

در این درس، سه روش صفحه‌بندی در DRF را یاد می‌گیرید.

  • PageNumberPagination رایج‌ترین روش. همان چیزی که در اکثر سایت‌ها می‌بینید. با پارامتر ?page=2.
  • LimitOffsetPagination روشی شبیه به APIهای بزرگ مثل گوگل. با پارامتر ?limit=10&offset=20.
  • CursorPagination برای داده‌های خیلی بزرگ و نرم‌افزارهای زمان‌واقعی. سریع‌تر و قابل اعتمادتر از دو روش قبلی.

همچنین یاد می‌گیرید چطور تنظیمات صفحه‌بندی را یک بار برای کل پروژه تعیین کنید. و چطور در یک ویو خاص، تنظیمات متفاوتی اعمال کنید.

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

چرا به صفحه بندی (Pagination) در DRF در نیاز داریم؟

مشکل دنیای واقعی

فرض کنید یک فروشگاه اینترنتی راه انداخته‌اید. حالا بعد از چند ماه، هزاران محصول در دیتابیس شما ثبت شده است. حالا یک مشتری وارد سایت می‌شود و صفحه لیست محصولات را باز می‌کند. اگر API شما همه هزاران محصول را یکجا برگرداند، چه اتفاقی می‌افتد؟

اولاً سرور باید یک کوئری غول‌پیکر بزند. همه رکوردها را از دیتابیس بخواند. بعد همه را به JSON تبدیل کند. حجم پاسخ ممکن است چندین مگابایت شود.

دوماً کاربر باید منتظر بماند تا این همه داده دانلود شود. روی گوشی موبایل و اینترنت ضعیف، این یعنی چند ده ثانیه انتظار.

سوماً مرورگر کاربر باید همه این داده را در حافظه نگه دارد. ممکن است کند شود یا حتی از کار بیفتد.

چهارم اینکه کاربر واقعاً به همه هزاران محصول نیاز ندارد. فقط می‌خواهد چند محصول اول را ببیند. بعد اگر خواست، بقیه را ببیند.

راه حل: صفحه‌بندی

صفحه‌بندی یعنی داده‌ها را تکه تکه کن. هر تکه را یک صفحه بنام. هر بار فقط یک صفحه را برگردان. کاربر صفحه اول را می‌خواهد. شما ده رکورد اول را می‌فرستی. کاربر دکمه «بعدی» را می‌زند. شما ده رکورد بعدی را می‌فرستی.

اینطوری:

  • سرور هر بار فقط مقدار کمی داده پردازش می‌کند
  • پاسخ سریع‌تر می‌رسد
  • پهنای باند کمتری مصرف می‌شود
  • کاربر بهتر می‌تواند داده‌ها را مرور کند

صفحه‌بندی در دنیای واقعی

اکثر APIهای بزرگی که هر روز استفاده می‌کنید، صفحه‌بندی دارند. اینستاگرام وقتی فید شما را نشان می‌دهد، همه پست‌ها را یکجا نمی‌فرستد. چند تای اول را می‌فرستد. وقتی به انتهای صفحه برسید، چند تای بعدی را می‌فرستد.

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

همه اینها از صفحه‌بندی استفاده می‌کنند.

بدون صفحه‌بندی چه اتفاقی می‌افتد؟

اگر صفحه‌بندی نداشته باشید، سه مشکل جدی پیدا می‌کنید.

  • اول: با بزرگ شدن دیتابیس، API شما کند می‌شود. مشتری‌ها شاکی می‌شوند.
  • دوم: سرور شما زیر بار سنگین ممکن است از کار بیفتد. مخصوصاً اگر چند کاربر همزمان درخواست بدهند.
  • سوم: کاربران موبایل که اینترنت محدود دارند، از حجم بالای پاسخ ناراحت می‌شوند.

صفحه‌بندی در DRF

خوشبختانه DRF صفحه‌بندی را از قبل برای شما آماده کرده است.

شما فقط باید انتخاب کنید کدام روش را می‌خواهید. و تنظیمات اولیه را انجام دهید. بقیه کارها را DRF خودش انجام می‌دهد.

سه روش اصلی در DRF وجود دارد:

  • PageNumberPagination: ساده و رایج. با شماره صفحه کار می‌کند
  • LimitOffsetPagination: شبیه به APIهای گوگل و فیسبوک
  • CursorPagination: برای داده‌های خیلی بزرگ و زمان‌واقعی

صفحه‌بندی با PageNumberPagination (شماره صفحه)

این روش ساده‌ترین و رایج‌ترین روش صفحه‌بندی. همان چیزی که در اکثر سایت‌ها می‌بینید.

کاربر با پارامتر page مشخص می‌کند کدام صفحه را می‌خواهد.

GET /api/products/?page=2

یعنی صفحه دوم. شما فرض کنید هر صفحه ۱۰ آیتم دارد. پس آیتم‌های ۱۱ تا ۲۰ را برمی‌گردانید.

قدم اول: تنظیمات پایه در فایل settings.py:

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10
}

با این دو خط، همه ویوهای لیست شما به طور خودکار صفحه‌بندی می‌شوند. هر صفحه ۱۰ آیتم دارد.

قدم دوم: خروجی صفحه‌بندی

وقتی کاربر درخواست می‌دهد:

GET /api/products/?page=2

پاسخ به این شکل خواهد بود:

{
    "count": 100,
    "next": "http://example.com/api/products/?page=3",
    "previous": "http://example.com/api/products/?page=1",
    "results": [
        {"id": 11, "name": "product 11"},
        {"id": 12, "name": "product 12"},
        ...
    ]
}
  • count: تعداد کل رکوردها (۱۰۰ تا)
  • next: آدرس صفحه بعدی (اگر وجود داشته باشد)
  • previous: آدرس صفحه قبلی (اگر وجود داشته باشد)
  • results: خود داده‌های این صفحه

این ساختار استاندارد است. فرانت‌اند می‌داند چطور آن را نمایش دهد.

قدم سوم: استفاده در ویو (بدون تنظیمات سراسری)

اگر نمی‌خواهید تنظیمات سراسری کنید، می‌توانید در هر ویو جداگانه صفحه‌بندی را اضافه کنید:

from rest_framework.pagination import PageNumberPagination
from rest_framework.generics import ListAPIView

class ProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    pagination_class = PageNumberPagination

اما باز هم باید PAGE_SIZE را جایی مشخص کنید. یا در settings.py یا با سفارشی‌سازی کلاس صفحه‌بندی.

قدم چهارم: شماره صفحه نامعتبر

اگر کاربر شماره صفحه‌ای بدهد که وجود ندارد (مثلاً صفحه ۲۰ در حالی که فقط ۵ صفحه دارید)، چه اتفاقی می‌افتد؟

DRF یک صفحه خالی برمی‌گرداند. نه خطای ۴۰۴. چون خود درخواست اشتباه نیست. فقط داده‌ای برای نمایش وجود ندارد.

{
    "count": 50,
    "next": null,
    "previous": "http://example.com/api/products/?page=5",
    "results": []
}

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

این روش برای ۹۰٪ پروژه‌ها مناسب است.

  • سایت‌های خبری
  • فروشگاه‌های اینترنتی
  • وبلاگ‌ها
  • پنل‌های مدیریت

هر جایی که کاربر عادی با صفحه‌بندی شماره‌دار آشنا است.

نکته مهم: ایندکس در دیتابیس

وقتی صفحه‌بندی می‌کنید، مطمئن شوید روی فیلدی که مرتب‌سازی می‌کنید ایندکس دارید.

مرتب‌سازی پیش‌فرض DRF روی pk (شناسه اصلی) است. که خودش ایندکس دارد. مشکلی نیست.

اما اگر مرتب‌سازی را تغییر دهید، ممکن است کوئری‌های شما کند شوند.

خطاهای رایج

اول: فراموشی PAGE_SIZE

اگر DEFAULT_PAGINATION_CLASS را تنظیم کنید اما PAGE_SIZE را نگذارید، خطا می‌گیرید.

دوم: صفحه‌بندی در ویوهای غیر List

صفحه‌بندی فقط برای ویوهایی که لیست برمی‌گردانند معنی دارد. مثل ListAPIView و ViewSet با اکشن list. در ویوهای retrieve (جزئیات) صفحه‌بندی نداریم.

سوم: تداخل با فیلتر و جستجو

صفحه‌بندی همیشه آخرین مرحله است. اول فیلترها اعمال می‌شوند، بعد جستجو، بعد مرتب‌سازی، و در آخر صفحه‌بندی. نگران نباشید. ترتیب درست است.

تمرین این بخش

یک API ساده با مدل Product بسازید و ۵۰ رکورد نمونه در آن ایجاد کنید.

PageNumberPagination را با PAGE_SIZE=5 تنظیم کنید.

در مرورگر آدرس‌های زیر را تست کنید:

  • صفحه اول: ?page=1
  • صفحه دوم: ?page=2
  • صفحه آخر: ?page=10
  • صفحه‌ای که وجود ندارد: ?page=20

خروجی JSON را در هر حالت ببینید.

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

تنظیمات پیش‌فرض PageNumberPagination برای خیلی از پروژه‌ها خوب است. اما نه همه. گاهی می‌خواهید اسم پارامتر صفحه را عوض کنید. مشتری شما می‌گوید «به جای page از p استفاده کن». گاهی می‌خواهید تعداد آیتم هر صفحه را بیشتر یا کمتر کنید. یک API عمومی شاید هر صفحه ۲۰ آیتم داشته باشد. اما یک API داخلی شرکت شاید ۱۰۰ آیتم نیاز داشته باشد. گاهی می‌خواهید ساختار خروجی را تغییر دهید. مثلاً به جای next و previous، چیزی بنویسید.

خوشبختانه سفارشی‌سازی PageNumberPagination خیلی ساده است.

قدم اول: ساختن کلاس سفارشی

یک فایل جدید به اسم paginations.py در اپ خود بسازید. یا اگر پروژه کوچک است، همان views.py یا utils.py می‌توانید بنویسید.

# paginations.py
from rest_framework.pagination import PageNumberPagination

class CustomPageNumberPagination(PageNumberPagination):
    page_size = 20
    page_query_param = 'p'
    page_size_query_param = 'page_size'
    max_page_size = 100

حالا بیایید هر کدام را توضیح بدهیم.

page_size – تعداد آیتم هر صفحه

page_size = 20

یعنی هر صفحه حداکثر ۲۰ آیتم دارد. کاربر نمی‌تواند این مقدار را تغییر دهد (مگر اینکه page_size_query_param را هم تنظیم کنید).

page_query_param – اسم پارامتر صفحه

page_query_param = 'p'

پیش‌فرض page است. با این تغییر، کاربر باید بنویسد:

GET /api/products/?p=2

باز هم کار می‌کند. ?page=2 دیگر کار نمی‌کند.

page_size_query_param – اجازه تغییر اندازه صفحه به کاربر

page_size_query_param = 'page_size'

با این تنظیم، کاربر می‌تواند خودش تعیین کند هر صفحه چند آیتم ببیند.

GET /api/products/?p=2&page_size=50

یعنی صفحه دوم را به من بده، اما هر صفحه ۵۰ آیتم داشته باشد.

نکته امنیتی: حتماً max_page_size را هم تنظیم کنید. وگرنه کاربر می‌تواند page_size=10000 بفرستد و سرور شما را سنگین کند.

max_page_size – حداکثر اندازه صفحه

max_page_size = 100

قدم دوم: تغییر ساختار خروجی

گاهی می‌خواهید کلیدهای خروجی را تغییر دهید. مثلاً به جای next بنویسید nextPage.

class CustomPageNumberPagination(PageNumberPagination):
    page_size = 20
    
    def get_paginated_response(self, data):
        return Response({
            'total_count': self.page.paginator.count,
            'next_page': self.get_next_link(),
            'previous_page': self.get_previous_link(),
            'results': data
        })

حالا خروجی به این شکل می‌شود:

{
    "total_count": 100,
    "next_page": "http://example.com/api/products/?page=3",
    "previous_page": "http://example.com/api/products/?page=1",
    "results": [...]
}

قدم سوم: استفاده در ویو

from .paginations import CustomPageNumberPagination

class ProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    pagination_class = CustomPageNumberPagination

قدم چهارم: استفاده سراسری از کلاس سفارشی

اگر می‌خواهید کل پروژه از این تنظیمات استفاده کند:

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'myapp.paginations.CustomPageNumberPagination',
}

مثال کامل: سناریوی واقعی

فرض کنید یک فروشگاه اینترنتی دارید. می‌خواهید:

  • هر صفحه ۱۵ محصول نشان داده شود
  • کاربر بتواند با پارامتر per_page تعداد محصولات هر صفحه را تغییر دهد (حداکثر ۵۰)
  • اسم پارامتر صفحه page باشد (همان پیش‌فرض)
  • در خروجی، به جای next و previous از آدرس کامل استفاده شود (که خودش انجام می‌دهد)
class ProductPagination(PageNumberPagination):
    page_size = 15
    page_size_query_param = 'per_page'
    max_page_size = 50

حالا کاربر می‌تواند:

# صفحه دوم، هر صفحه ۱۵ محصول (پیش‌فرض)
GET /api/products/?page=2

# صفحه دوم، هر صفحه ۳۰ محصول
GET /api/products/?page=2&per_page=30

# صفحه اول، هر صفحه ۵۰ محصول (حداکثر مجاز)
GET /api/products/?per_page=50

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

ویژگی توضیح مثال
page_size تعداد آیتم هر صفحه page_size = 20
page_query_param نام پارامتر صفحه page_query_param = 'p'
page_size_query_param نام پارامتر اندازه صفحه page_size_query_param = 'limit'
max_page_size حداکثر اندازه صفحه max_page_size = 100
get_paginated_response ساختار خروجی تغییر کلیدهای JSON
get_next_link منطق لینک صفحه بعد حذف یا تغییر آدرس
get_previous_link منطق لینک صفحه قبل حذف یا تغییر آدرس

نکات مهم

نکته اول: page_size_query_param بدون max_page_size خطرناک است

اگر به کاربر اجازه دهید هر تعداد که می‌خواهد تعیین کند، ممکن است یکباره ۱۰ هزار رکورد درخواست بدهد. هم سرور شما اذیت می‌شود، هم شبکه. حتماً max_page_size را تنظیم کنید.

نکته دوم: ثابت نگه داشتن page_size برای برخی ویوها

اگر در یک ویو خاص می‌خواهید صفحه‌بندی ثابت داشته باشد (کاربر نتواند تغییر دهد)، page_size_query_param = None بگذارید.

نکته سوم: حذف لینک‌های next و previous در خروجی

اگر نمی‌خواهید next و previous در خروجی باشند، در get_paginated_response آن‌ها را حذف کنید:

def get_paginated_response(self, data):
    return Response({
        'count': self.page.paginator.count,
        'results': data
    })

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

یک کلاس صفحه‌بندی سفارشی بسازید با مشخصات زیر:

  • هر صفحه ۱۰ آیتم (پیش‌فرض)
  • کاربر بتواند با پارامتر limit تعداد آیتم هر صفحه را تغییر دهد (حداکثر ۵۰)
  • نام پارامتر صفحه page_number باشد (نه page)
  • خروجی شامل total (به جای count) و next_link (به جای next) باشد

سپس این کلاس را در یکی از ویوهای خود استفاده کنید و با Postman تست کنید.

LimitOffsetPagination (محدودیت و آفست)

PageNumberPagination از شماره صفحه استفاده میکرد. مثلا کاربر می‌گوید «صفحه ۲ را بده». LimitOffsetPagination اما روش دیگری دارد. کاربر می‌گوید «از رکورد ۲۰ به بعد، ۱۰ تا بده». این روش در APIهای بزرگی مثل گوگل، فیسبوک و توییتر استفاده می‌شود.

دو پارامتر دارد:

  • limit: حداکثر تعداد رکوردهایی که می‌خواهی (مثل ?limit=10)
  • offset: از چندمین رکورد شروع کن (مثل ?offset=20)

تفاوت با روش شماره صفحه

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

در روش آفست، کاربر دقیقاً می‌گوید از کجا شروع کن.

مثال:

?limit=10&offset=0   → رکوردهای ۱ تا ۱۰
?limit=10&offset=10  → رکوردهای ۱۱ تا ۲۰
?limit=10&offset=20  → رکوردهای ۲۱ تا ۳۰

قدم اول: تنظیمات پایه

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    'PAGE_SIZE': 10  # مقدار پیش‌فرض limit
}

PAGE_SIZE همان limit پیش‌فرض است. اگر کاربر limit نفرستد، این مقدار استفاده می‌شود.

قدم دوم: خروجی صفحه‌بندی

وقتی کاربر درخواست می‌دهد:

GET /api/products/?limit=10&offset=20

پاسخ به این شکل خواهد بود:

{
    "count": 100,
    "next": "http://example.com/api/products/?limit=10&offset=30",
    "previous": "http://example.com/api/products/?limit=10&offset=10",
    "results": [
        {"id": 21, "name": "product 21"},
        {"id": 22, "name": "product 22"},
        ...
    ]
}
  • count: تعداد کل رکوردها
  • next: آدرس صفحه بعدی (با offset بیشتر)
  • previous: آدرس صفحه قبلی (با offset کمتر)
  • results: خود داده‌های این صفحه

قدم سوم: سفارشی‌سازی LimitOffsetPagination

مثل روش قبلی، می‌توانید آن را سفارشی کنید:

from rest_framework.pagination import LimitOffsetPagination

class CustomLimitOffsetPagination(LimitOffsetPagination):
    default_limit = 20           # تعداد پیش‌فرض (جایگزین PAGE_SIZE)
    limit_query_param = 'limit'  # نام پارامتر محدودیت
    offset_query_param = 'offset' # نام پارامتر آفست
    max_limit = 100              # حداکثر مقدار مجاز limit

توضیح پارامترها:

  • default_limit: اگر کاربر limit نفرستد، این مقدار استفاده می‌شود
  • limit_query_param: اسم پارامتر محدودیت (می‌توانید به 'page_size' یا 'per_page' تغییر دهید)
  • offset_query_param: اسم پارامتر آفست (می‌توانید به 'start' تغییر دهید)
  • max_limit: حداکثر مقدار limit. کاربر نمی‌تواند بیشتر از این درخواست بدهد.

مثال: سفارشی‌سازی برای API فروشگاه

class ProductOffsetPagination(LimitOffsetPagination):
    default_limit = 15
    limit_query_param = 'per_page'
    offset_query_param = 'start'
    max_limit = 50

حالا کاربر می‌تواند:

GET /api/products/?per_page=10&start=20

یعنی ۱۰ تا محصول، از رکورد ۲۰ به بعد.

مزایای LimitOffsetPagination

مزیت اول: انعطاف بیشتر

کاربر می‌تواند دقیقاً مشخص کند از کجا شروع کند. برای این که برود به رکورد ۱۰۰۰، لازم نیست صفحه ۱۰۰ را حساب کند. کافی است offset=1000 بفرستد.

مزیت دوم: مناسب برای APIهای جستجو

در نتایج جستجوی گوگل، شما شماره صفحه نمی‌بینید. بلکه «نتایج ۲۰ تا ۳۰ از ۱۰۰۰» می‌بینید. این دقیقاً همان limit و offset است.

مزیت سوم: رفتن به عمق دیتابیس

با offset=10000 می‌توانید سریعاً به رکوردهای قدیمی دسترسی پیدا کنید. در روش شماره صفحه، باید صفحه ۱۰۰۰ را محاسبه کنید.

معایب LimitOffsetPagination

عیب اصلی: کندی در offsetهای بزرگ

وقتی می‌نویسید offset=10000، دیتابیس مجبور است ۱۰۰۰۰ رکورد اول را بشمارد و بعد از آن شروع کند. هرچه offset بزرگتر باشد، کندتر می‌شود.

در یک دیتابیس با میلیون‌ها رکورد، offset=500000 می‌تواند چند ثانیه طول بکشد.

راه حل: برای داده‌های خیلی بزرگ، از CursorPagination استفاده کنید که در زیرعنوان بعدی یاد می‌گیرید.

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

سناریو پیشنهاد
API عمومی با داده‌های کم (کمتر از ۱۰ هزار رکورد) خوب است
API داخلی شرکت با دسترسی مدیریت خوب است
API جستجو با نتایج زیاد خوب است
داده‌های خیلی بزرگ (میلیون‌ها رکورد) نه، از CursorPagination استفاده کنید
زمانی که کاربر نیاز به پرش به عمق داده دارد خوب است

استفاده در ویو

from .paginations import CustomLimitOffsetPagination

class ProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    pagination_class = CustomLimitOffsetPagination

مقایسه سریع با PageNumberPagination

ویژگی PageNumberPagination LimitOffsetPagination
پارامترها ?page=2 ?limit=10&offset=20
درک برای کاربر ساده و آشنا کمی حرفه‌ای‌تر
رفتن به عمق داده سخت (باید صفحه را حساب کند) آسان (offset=1000)
عملکرد در offset بزرگ خوب کند می‌شود
مناسب برای اکثر پروژه‌ها APIهای جستجو و داده‌های متوسط

تمرین 

یک API ساده با مدل Product و ۱۰۰ رکورد نمونه بسازید.

LimitOffsetPagination را با default_limit=5 و max_limit=20 تنظیم کنید.

درخواست‌های زیر را در Postman تست کنید و خروجی را ببینید:

  • GET /api/products/?limit=5&offset=0 (صفحه اول)
  • GET /api/products/?limit=5&offset=5 (صفحه دوم)
  • GET /api/products/?limit=20&offset=40 (۲۰ تا از رکورد ۴۱)
  • GET /api/products/?limit=50&offset=100 (ببینید max_limit=20 چطور این را محدود می‌کند)

CursorPagination (برای داده‌های خیلی بزرگ) در DRF

مشکل دو روش قبلی

PageNumberPagination و LimitOffsetPagination یک مشکل بزرگ دارند. وقتی داده‌ها خیلی زیاد می‌شوند، کند می‌شوند. چرا؟ چون دیتابیس برای پیدا کردن رکوردهای صفحه ۱۰۰۰ یا آفست ۱۰۰۰۰، مجبور است همه رکوردهای قبلی را بشمارد. هر چه صفحه بالاتر می‌رود، کار سنگین‌تر می‌شود. در یک دیتابیس با میلیون‌ها رکورد، صفحه ۵۰۰۰ ممکن است چند ثانیه طول بکشد. کاربر از این انتظار خوشش نمی‌آید.

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

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

به جای اینکه بگوید «رکورد ۱۰۰۰ به بعد را بده»، می‌گوید «بعد از این رکورد خاص را بده». کاربر یک نشانگر (cursor) دریافت می‌کند. این نشانگر موقعیت دقیق در دیتابیس را مشخص می‌کند. برای رفتن به صفحه بعد، همان نشانگر را برمی‌گرداند. دیتابیس مجبور نیست چیزی را بشمارد. فقط می‌گوید «از اینجا به بعد را بیار». این خیلی سریع‌تر است.

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

این روش برای داده‌های خیلی بزرگ طراحی شده است.

  • میلیون‌ها رکورد در دیتابیس
  • APIهای زمان‌واقعی (Real-time)
  • فیدهای خبری مثل اینستاگرام و توییتر
  • لاگ‌های سرور
  • داده‌های حسگرها (IoT)

اگر دیتابیس شما کمتر از ۱۰ هزار رکورد دارد، نیازی به این روش نیست. روش‌های قبلی کافی هستند.

قدم اول: تنظیمات پایه

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.CursorPagination',
    'PAGE_SIZE': 10
}

قدم دوم: سفارشی‌سازی ضروری

CursorPagination نیاز دارد بداند بر اساس چه فیلدی مرتب‌سازی کند. این فیلد باید سه ویژگی داشته باشد:

  • یکتا باشد (مثل id)
  • قابل مقایسه باشد (عدد یا تاریخ)
  • ایندکس داشته باشد
from rest_framework.pagination import CursorPagination

class CustomCursorPagination(CursorPagination):
    page_size = 10
    cursor_query_param = 'cursor'  # نام پارامتر پیش‌فرض
    ordering = '-created_at'       # مرتب‌سازی بر اساس تاریخ (جدیدترین اول)

توضیح ordering:

  • 'created_at' → قدیمی‌ترین اول
  • '-created_at' → جدیدترین اول (علامت منفی یعنی نزولی)
  • 'id' → بر اساس شناسه

قدم سوم: خروجی صفحه‌بندی

وقتی کاربر درخواست می‌دهد:

GET /api/products/

پاسخ به این شکل خواهد بود:

{
    "next": "http://example.com/api/products/?cursor=cD0yMDI0LTAxLTE1KzEyJTNBMDAlM0EwMA%3D%3D",
    "previous": null,
    "results": [
        {"id": 100, "name": "product 100", "created_at": "2024-01-15T12:00:00Z"},
        {"id": 99, "name": "product 99", "created_at": "2024-01-14T10:30:00Z"},
        ...
    ]
}

توجه کنید که next یک رشته رمزنگاری شده است. کاربر نمی‌تواند آن را دستکاری کند. این همان cursor است.

برای رفتن به صفحه بعد، کاربر همین آدرس را باز می‌کند. DRF خودکار cursor را می‌خواند و ادامه می‌دهد.

قدم چهارم: استفاده در ویو

from .paginations import CustomCursorPagination

class ProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    pagination_class = CustomCursorPagination

تفاوت اصلی با روش‌های قبلی

در روش‌های قبلی، کاربر می‌توانست مستقیم بگوید «برو به صفحه ۵» یا «از رکورد ۱۰۰ شروع کن». در روشCursorPagination، کاربر نمی‌تواند به صفحه دلخواه برود. فقط می‌تواند «بعدی» یا «قبلی» را بزند. این محدودیت عمدی است. چون اجازه پرش به هر صفحه، همان مشکل کندی را برمی‌گرداند.

مثال واقعی: فید اینستاگرام

اینستاگرام از روش CursorPagination استفاده می‌کند. شما اسکرول می‌کنید. برنامه می‌گوید «بعد از آخرین پستی که دیدی، ۱۰ تای بعدی را بده». هرگز نمی‌توانید بگویید «برو به صفحه ۱۰۰». چون فید شما مدام در حال تغییر است. صفحه ۱۰۰ امروز با صفحه ۱۰۰ فردا فرق دارد.

سفارشی‌سازی پیشرفته

class FeedPagination(CursorPagination):
    page_size = 15
    ordering = '-published_at'
    cursor_query_param = 'c'
    
    def get_ordering(self, request, queryset, view):
        # می‌توانید بر اساس کاربر یا شرایط، مرتب‌سازی را تغییر دهید
        if request.user.is_staff:
            return '-created_at'
        return '-published_at'

مزایا

  • سرعت بالا: حتی در دیتابیس‌های چند میلیون رکوردی، سریع است
  • مناسب برای فیدهای زنده: داده‌های جدید به راحتی اضافه می‌شوند بدون اینکه باعث جا به جایی صفحات شوند
  • قابل اعتماد: کاربر نمی‌تواند cursor را دستکاری کند

معایب

  • عدم پشتیبانی از پرش به صفحه دلخواه: کاربر فقط می‌تواند «بعدی» و «قبلی» بزند
  • پیچیدگی بیشتر: درک آن برای تازه‌کارها سخت‌تر است
  • وابستگی به فیلد مرتب‌سازی: فیلد باید یکتا و قابل مقایسه باشد

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

حجم داده روش پیشنهادی
کمتر از ۱۰۰۰ رکورد PageNumberPagination
۱۰۰۰ تا ۱۰۰ هزار رکورد LimitOffsetPagination یا PageNumberPagination
بیشتر از ۱۰۰ هزار رکورد CursorPagination
فیدهای زنده و زمان‌واقعی CursorPagination
کاربر باید به صفحه دلخواه برود PageNumberPagination (با کش کردن)

نکته مهم: ایندکس دیتابیس

در CursorPagination، فیلدی که برای ordering انتخاب می‌کنید حتماً باید در دیتابیس ایندکس داشته باشد. بدون ایندکس، حتی این روش هم کند می‌شود.

# models.py
class Product(models.Model):
    created_at = models.DateTimeField(db_index=True)  # ایندکس

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

یک مدل Log با فیلدهای message و created_at بسازید. از یک اسکریپت استفاده کنید تا ۱۰۰ هزار رکورد لاگ در دیتابیس بریزید.

CursorPagination را با ordering = '-created_at' تنظیم کنید. در مرورگر آدرس را باز کنید و ببینید سرعت پاسخ چقدر است.

سپس همان درخواست را با PageNumberPagination امتحان کنید. تفاوت سرعت را در صفحه‌های آخر مقایسه کنید.

تنظیم سراسری صفحه‌بندی در settings.py

چرا تنظیم سراسری؟

فرض کنید ده ویو لیست در پروژه خود دارید. اگر بخواهید در هر کدام جداگانه صفحه‌بندی را فعال کنید، ده بار کد تکراری می‌نویسید. یک جا را فراموش می‌کنید. یک جا تنظیمات متفاوت می‌گذارید. پروژه نامرتب می‌شود. تنظیم سراسری یعنی یک بار تعیین کنید همه ویوهای لیست از چه روش و چه تنظیماتی استفاده کنند. DRF خودکار آن را به همه اعمال می‌کند.

تنظیم پایه در فایل settings.py

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20
}

با این دو خط، تمام ویوهای ListAPIView و هر ویوی دیگری که لیست برمی‌گرداند، به طور خودکار صفحه‌بندی می‌شوند. هر صفحه ۲۰ آیتم دارد.

انتخاب روش صفحه‌بندی در تنظیمات سراسری

سه روش اصلی را می‌توانید به عنوان پیش‌فرض پروژه انتخاب کنید:

روش اول: PageNumberPagination

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 25
}

روش دوم: LimitOffsetPagination

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
    'PAGE_SIZE': 25  # default_limit
}

روش سوم: CursorPagination

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'myapp.paginations.CustomCursorPagination',
    'PAGE_SIZE': 15
}

توجه کنید که CursorPagination نیاز به سفارشی‌سازی دارد. نمی‌توانید مستقیماً از کلاس پیش‌فرض استفاده کنید. چون باید ordering را مشخص کنید.

استفاده از کلاس سفارشی در تنظیمات سراسری

اگر خودتان یک کلاس صفحه‌بندی سفارشی ساخته‌اید، می‌توانید آن را به عنوان پیش‌فرض معرفی کنید (فایل settings.py):

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'myapp.paginations.ProductPagination',
    'PAGE_SIZE': 15
}

فایل paginations.py:

class ProductPagination(PageNumberPagination):
    page_size = 15
    page_size_query_param = 'per_page'
    max_page_size = 50

تأثیر تنظیمات سراسری روی همه ویوها

بعد از این تنظیم، تمام ویوهای لیست شما صفحه‌بندی می‌شوند. حتی آنهایی که خودتان قبلاً pagination_class را تنظیم نکرده بودید.

# این ویو بدون هیچ تنظیم اضافه‌ای صفحه‌بندی می‌شود
class ProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer

چه موقع تنظیم سراسری خوب است؟

مزایا:

  • کد کمتر و تمیزتر
  • رفتار یکسان در کل پروژه
  • تغییر در یک جا، تأثیر در همه جا
  • کمتر خطا می‌دهد (چون چیزی را فراموش نمی‌کنید)

معایب:

  • نمی‌توانید برای ویوهای مختلف تنظیمات متفاوت داشته باشید (مگر اینکه override کنید)
  • اگر پروژه شما انواع مختلف داده با حجم متفاوت دارد، یک اندازه برای همه ممکن است مناسب نباشد

ترکیب تنظیمات سراسری و محلی

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

class SpecialProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    pagination_class = None  # غیرفعال کردن صفحه‌بندی

یا استفاده از کلاس متفاوت:

class VIPProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    pagination_class = CustomVIPPagination  # کلاس متفاوت با page_size=50

یک اشتباه رایج

بعضی تازه‌کارها DEFAULT_PAGINATION_CLASS را تنظیم می‌کنند اما PAGE_SIZE را فراموش می‌کنند.

نتیجه: خطا می‌گیرند. چون DRF نمی‌داند هر صفحه چند آیتم نشان بدهد.

هر وقت DEFAULT_PAGINATION_CLASS را تنظیم کردید، حتماً PAGE_SIZE را هم بنویسید.

مثال کامل: پروژه فروشگاهی

فایل settings.py

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'shop.paginations.ShopPagination',
    'PAGE_SIZE': 20
}

فایل paginations.py

# shop/paginations.py
from rest_framework.pagination import PageNumberPagination

class ShopPagination(PageNumberPagination):
    page_size = 20
    page_size_query_param = 'per_page'
    max_page_size = 100

فایل views.py

# shop/views.py
# این ویو از تنظیمات سراسری استفاده می‌کند
class ProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    # بدون pagination_class

# این ویو تنظیمات خاص خودش را دارد
class FlashSaleListView(ListAPIView):
    queryset = FlashSale.objects.all()
    serializer_class = FlashSaleSerializer
    pagination_class = None  # تخفیف‌های لحظه‌ای بدون صفحه‌بندی

تمرین 

در پروژه خود، تنظیمات سراسری صفحه‌بندی را با روش PageNumberPagination و PAGE_SIZE=10 فعال کنید.

سپس دو ویو بسازید:

  • یکی که از همین تنظیمات سراسری استفاده کند
  • یکی که صفحه‌بندی را کاملاً غیرفعال کند (pagination_class = None)

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

تنظیم محلی صفحه‌بندی در هر ویو

چرا تنظیم محلی؟

تنظیمات سراسری برای همه ویوها یکسان است. اما همیشه اینطور نیست. گاهی یک ویو خاص داده‌های حجیم‌تری دارد. مثلاً صفحه محصولات در مقایسه با صفحه نظرات کاربران. گاهی یک API عمومی باید هر صفحه ۲۰ آیتم داشته باشد، اما API داخلی مدیریت باید ۱۰۰ آیتم داشته باشد. گاهی هم باید یک ویو اصلاً صفحه‌بندی نداشته باشد. مثل API آخرین مقالات که فقط ۵ تا می‌خواهد، نه صفحه اول و دوم.

تنظیم محلی یعنی در همان ویو مشخص کنید چه رفتاری داشته باشد. بقیه ویوها از تنظیمات سراسری استفاده می‌کنند.

تنظیم محلی با pagination_class

ساده‌ترین راه. در هر ویو که نیاز دارید، pagination_class را تعیین کنید.

from rest_framework.pagination import PageNumberPagination
from rest_framework.generics import ListAPIView

class ProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    pagination_class = PageNumberPagination

البته این کد هنوز PAGE_SIZE را مشخص نکرده. می‌توانید از کلاس سفارشی استفاده کنید.

استفاده از کلاس سفارشی در یک ویو خاص

فایل paginations.py

# paginations.py
class SmallPagePagination(PageNumberPagination):
    page_size = 5
    page_size_query_param = 'per_page'
    max_page_size = 20

class LargePagePagination(PageNumberPagination):
    page_size = 100
    page_size_query_param = 'per_page'
    max_page_size = 500

فایل views.py

# views.py
from .paginations import SmallPagePagination, LargePagePagination

class PublicProductListView(ListAPIView):
    queryset = Product.objects.filter(is_public=True)
    serializer_class = ProductSerializer
    pagination_class = SmallPagePagination  # هر صفحه ۵ تا

class AdminProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    pagination_class = LargePagePagination  # هر صفحه ۱۰۰ تا

غیرفعال کردن صفحه‌بندی در یک ویو خاص

بعضی ویوها اصلاً به صفحه‌بندی نیاز ندارند. مثلاً API که فقط ۵ مقاله آخر را نشان می‌دهد.

class LatestArticlesView(ListAPIView):
    queryset = Article.objects.all().order_by('-created_at')[:5]
    serializer_class = ArticleSerializer
    pagination_class = None  # صفحه‌بندی غیرفعال

نکته: اگر pagination_class = None نگذارید، حتی با [:5] هم صفحه‌بندی اعمال می‌شود. نتیجه اشتباه می‌شود.

روش دیگر: بازنویسی get_paginator

اگر نیاز به منطق پیچیده‌تری دارید، می‌توانید متد paginate_queryset را بازنویسی کنید:

class ConditionalProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    
    def paginate_queryset(self, queryset):
        # اگر کاربر خاصی است، صفحه‌بندی را حذف کن
        if self.request.user.is_staff:
            return None
        return super().paginate_queryset(queryset)

اولویت با تنظیم محلی

اگر هم تنظیم سراسری داشته باشید و هم در ویو pagination_class را تعیین کنید، تنظیم محلی اولویت دارد.

تنظیم محلی (در ویو) > تنظیم سراسری (در settings.py)

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

مثال عملی: سه ویو با سه رفتار متفاوت

فایل settings.py

# settings.py
REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 20  # تنظیم سراسری: هر صفحه ۲۰ تا
}

فایل views.py

# views.py
from .paginations import SmallPagination, NoPagination

# ویو ۱: از تنظیم سراسری استفاده می‌کند (هر صفحه ۲۰ تا)
class ProductListView(ListAPIView):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    # بدون pagination_class

# ویو ۲: تنظیم محلی با اندازه متفاوت (هر صفحه ۵ تا)
class FlashSaleListView(ListAPIView):
    queryset = FlashSale.objects.all()
    serializer_class = FlashSaleSerializer
    pagination_class = SmallPagination  # page_size=5

# ویو ۳: صفحه‌بندی کاملاً غیرفعال
class LatestProductsView(ListAPIView):
    queryset = Product.objects.all().order_by('-created_at')[:10]
    serializer_class = ProductSerializer
    pagination_class = None

نکات مهم

نکته اول: pagination_class = None فقط صفحه‌بندی را غیرفعال می‌کند. تأثیری روی فیلتر، جستجو و مرتب‌سازی ندارد.

نکته دوم: اگر از ViewSet استفاده می‌کنید، همان روش pagination_class جواب می‌دهد:

class ProductViewSet(ModelViewSet):
    queryset = Product.objects.all()
    serializer_class = ProductSerializer
    pagination_class = SmallPagination

نکته سوم: اگر pagination_class را تعیین کنید اما PAGE_SIZE در آن کلاس تعریف نشده باشد، DRF از PAGE_SIZE تنظیمات سراسری استفاده می‌کند (اگر وجود داشته باشد).

چه موقع تنظیم محلی بهتر از سراسری است؟

سناریو راه حل
اکثر ویوها رفتار یکسان دارند، یکی دو ویو متفاوت تنظیم سراسری + تنظیم محلی برای ویوهای متفاوت
هر ویو اندازه صفحه متفاوتی نیاز دارد تنظیم محلی برای همه
پروژه کوچک با ۲-۳ ویو تنظیم محلی برای هر کدام (بدون تنظیم سراسری)
پروژه بزرگ با ویوهای متنوع تنظیم سراسری برای پیش‌فرض + تنظیم محلی برای موارد خاص

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

سه ویو زیر را بسازید:

  • PublicProductListView – از تنظیمات سراسری پروژه استفاده کند (شما قبلاً PAGE_SIZE=10 تنظیم کرده‌اید)
  • AdminProductListView – هر صفحه ۵۰ آیتم نشان دهد
  • FeaturedProductListView – صفحه‌بندی نداشته باشد و فقط ۳ محصول ویژه را برگرداند

هر سه را در Postman تست کنید و تفاوت خروجی را ببینید.

جمع‌بندی درس صفحه‌بندی در DRF

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

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

  • PageNumberPagination ساده و رایج. همان چیزی که در اکثر سایت‌ها می‌بینید. با پارامتر ?page=2. برای اکثر پروژه‌ها همین کافی است.
  • LimitOffsetPagination انعطاف بیشتری دارد. کاربر می‌گوید «از رکورد ۲۰ به بعد، ۱۰ تا بده». برای APIهای جستجو و زمانی که کاربر نیاز به پرش به عمق داده دارد، مناسب است.
  • CursorPagination برای داده‌های خیلی بزرگ. میلیون‌ها رکورد. فیدهای زمان‌واقعی. این روش کندی روش‌های قبلی را ندارد، اما کاربر نمی‌تواند به صفحه دلخواه پرش کند.

سفارشی‌سازی هر سه روش را یاد گرفتید. تغییر تعداد آیتم هر صفحه. تغییر نام پارامترها. محدود کردن حداکثر اندازه. حتی تغییر ساختار خروجی JSON. تنظیمات سراسری در settings.py را دیدید. یک بار تنظیم کنید، همه ویوها از آن استفاده می‌کنند. برای پروژه‌هایی که اکثر ویوها رفتار مشابهی دارند، عالی است. و در آخر، تنظیم محلی در هر ویو. برای مواقعی که یک ویو خاص نیاز به رفتار متفاوت دارد. یا اصلاً نباید صفحه‌بندی داشته باشد.

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

۱. صفحه‌بندی را از روز اول فعال کنید. حتی اگر الان داده زیادی ندارید. بعداً اضافه کردن آن سخت‌تر است.

۲. برای شروع، PageNumberPagination با PAGE_SIZE=20 انتخاب مطمئنی است.

۳. اگر داده‌هایتان از صد هزار رکورد گذشت، سراغ CursorPagination بروید.

۴. همیشه max_page_size را تنظیم کنید. از سرور خود در برابر درخواست‌های سنگین محافظت کنید.

۵. تنظیمات سراسری را برای پیش‌فرض پروژه بگذارید. تنظیمات محلی را برای ویوهای خاص.

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

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

اگر API شما تغییر کند، چه اتفاقی برای کاربرانی می‌افتد که هنوز از نسخه قدیمی استفاده می‌کنند؟

فرض کنید فیلد name را به full_name تغییر می‌دهید. اپلیکیشن موبایل قدیمی کاربران هنوز name را می‌فرستد. همه چیز خراب می‌شود.

راه حل: نسخه‌بندی API.

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