Asosiy kontentga o‘tish

Maqolalar

Darslik · 2026-08-17 · ~6 daqiqa o‘qiladi · boshlang'ich

DRF filtrlash va pagination: «hammasini» emas, «keraklisini» qaytaring

#python #django #drf #api #performance

Mundarija

GET /api/movies/ — va javobda bazadagi hamma film. Loyihaning birinchi haftasida bu hech kimga xalaqit bermaydi, chunki filmlar 50 ta. Bir yildan keyin ular 50 ming bo'ladi va o'sha endpoint har chaqiruvda megabaytlab JSON qaytara boshlaydi: mobil ilova qotadi, server terlaydi, hech kim esa bu javobning 20 tadan ortig'ini baribir o'qimaydi. Shuning uchun ro'yxat endpoint'ining ikkita majburiy jihozi bor: pagination (bo'lib berish) va filtrlash (so'ralganini berish). Avvalgi qismda hujjatlagan Movies API'ni endi shu jihozlar bilan to'ldiramiz.

Pagination: birinchi kundan, global qilib#

DRF'da sahifalash bitta sozlama bilan butun loyihaga yoqiladi:

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

Javob shakli endi o'zgaradi — mijozlarga aynan shu shaklni va'da qilasiz:

curl "http://127.0.0.1:8000/api/movies/?page=2"
{
  "count": 50000,
  "next": "http://127.0.0.1:8000/api/movies/?page=3",
  "previous": "http://127.0.0.1:8000/api/movies/?page=1",
  "results": [ ... 20 ta film ... ]
}

Mijoz next bo'yicha yuraveradi — sahifa raqamini o'zi hisoblashi shart emas. Sahifa hajmini mijozga ham berish kerak bo'lsa, PageNumberPaginationni meros olib page_size_query_param = "page_size" qo'shasiz, lekin max_page_size bilan chegaralab: aks holda kimdir ?page_size=1000000 deb eski muammoni qaytarib beradi.

Katta jadvalda page raqami sekinlashadi

?page=2500 degani bazaga "50 000 qator sanab o'tib, keyingisini ber" degani — OFFSET chuqurlashgan sari sekinlashadi. Ro'yxati millionlab yozuvli API'larda shuning uchun CursorPagination ishlatiladi: u "shu yozuvdan keyingilarini ber" deb ishlaydi va tezligi chuqurlikka bog'liq emas. Boshlang'ich loyihaga PageNumber yetadi, lekin bu chegarani bilib qo'ygan yaxshi.

Filtrlash: django-filter bilan#

Mijozga "faqat 2019-yilgi drama filmlar" kerak. Buni view ichida qo'lda if bilan yozish mumkin, lekin standart yechim tayyor:

uv add django-filter
# config/settings.py
INSTALLED_APPS = [
    # ...
    "django_filters",
]

REST_FRAMEWORK = {
    # ...
    "DEFAULT_FILTER_BACKENDS": [
        "django_filters.rest_framework.DjangoFilterBackend",
        "rest_framework.filters.SearchFilter",
        "rest_framework.filters.OrderingFilter",
    ],
}

View'da esa qaysi maydonlar bo'yicha nima mumkinligini e'lon qilamiz:

# apps/movies/views.py
class MovieViewSet(viewsets.ModelViewSet):
    queryset = Movie.objects.select_related("genre")
    serializer_class = MovieSerializer

    # aniq moslik filtrlari: ?genre=drama&year=2019
    filterset_fields = ["genre__slug", "year"]

    # matnli qidiruv: ?search=matrix — sarlavha va tavsif bo'ylab
    search_fields = ["title", "description"]

    # tartiblash: ?ordering=-rating (minus — kamayish tartibi)
    ordering_fields = ["rating", "year"]
    ordering = ["-year"]  # default tartib — doim aniq yozib qo'ying

Uchala backend birga ishlaydi va zanjir bo'lib qo'shiladi:

curl "http://127.0.0.1:8000/api/movies/?genre__slug=drama&year=2019&ordering=-rating"

ordering = ["-year"] qatori mayda ko'rinsa ham yozilishi shart: default tartibsiz PostgreSQL qatorlarni "qanday qulay bo'lsa shunday" qaytaradi va pagination sahifalari orasida yozuvlar takrorlanishi yoki tushib qolishi mumkin — mijoz ro'yxatni varaqlaganda bir film ikki marta chiqqani shundan.

Oraliq filtrlar: FilterSet bilan#

filterset_fields aniq moslik uchun yetarli, lekin "reytingi 7 dan yuqori" kabi oraliq so'rovlarga maxsus klass kerak:

# apps/movies/filters.py
import django_filters

from apps.movies.models import Movie


class MovieFilter(django_filters.FilterSet):
    # ?min_rating=7.5 — gte: katta yoki teng
    min_rating = django_filters.NumberFilter(field_name="rating", lookup_expr="gte")
    # ?year_after=2015&year_before=2020 — oraliq
    year_after = django_filters.NumberFilter(field_name="year", lookup_expr="gte")
    year_before = django_filters.NumberFilter(field_name="year", lookup_expr="lte")

    class Meta:
        model = Movie
        fields = ["genre__slug"]
# apps/movies/views.py — filterset_fields o'rniga:
    filterset_class = MovieFilter

Yoqimli tomoni: avvalgi qismdagi Swagger sahifasi bu parametrlarning hammasini o'zi ko'rsatadi — filtr qo'shdingiz, hujjat yangilandi.

Tezlikni unutmang#

Filtr va qidiruv qo'shilgach, bazaga tushadigan so'rovlar shakli o'zgaradi — ikki narsani tekshirib qo'ying. Filtrlanadigan ustunlarda indeks bo'lsin (genre, year, rating — qaysi biri bo'yicha ko'p so'ralsa), buni EXPLAIN bilan tekshirish usulini alohida yozganman. Va ro'yxat serializer'ida bog'liq jadvallar bo'lsa, select_related/prefetch_related turganiga ishonch hosil qiling — aks holda 20 talik sahifa 21 ta SQL so'rov beradi, bu N+1 muammosining klassik ko'rinishi.

Yakun#

Ro'yxat endpoint'ining to'liq jihozi shu: global pagination (next/previous bilan), aniq moslik uchun filterset_fields, matn uchun SearchFilter, tartib uchun OrderingFilter va default ordering — sahifalar sakramasligi uchun. Oraliqlar kerak bo'lganda FilterSet klassi bor, hammasi esa Swagger hujjatiga o'z-o'zidan tushib turadi.

Shu bilan o'quv turkumi ham yakuniga yetdi — endi Netflix-uslub API loyihasi ostida uning har qismini ochib beradigan maqolalar turadi.