Maqolalar 8 daqiqa 1119 ko‘rish
DRF filtrlash va pagination: «hammasini» emas, «keraklisini» qaytaring
Ro'yxat endpoint'i bazadagi hamma yozuvni qaytarsa, u vaqt bombasi: bugun 50 ta film, bir yildan keyin 50 ming. django-filter bilan filtrlash, qidiruv, tartiblash va sahifalashni Netflix-uslub API misolida to'liq sozlaymiz.
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 — o'sha
qismdagi Movie modeli (janr Genre jadvaliga bog'langan) va
MovieSerializer bilan.
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",
# DRF'ning OrderingFilter'i o'rniga — sababi pastda
"apps.core.filters.StableOrderingFilter",
],
}
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__slug=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"]
# default tartib — doim aniq va NOYOB: yili bir xil filmlar orasini id hal qiladi
ordering = ["-year", "id"]
Parametr nomi maydon yo'li bilan bir xil: genre__slug. ?genre=drama
yozilsa, django-filter bunday parametrni bilmaydi va jimgina e'tiborsiz
qoldiradi — sinovda bu so'rov 50 000 filmning hammasini qaytardi.
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 qatori mayda ko'rinsa ham yozilishi shart: 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.
Lekin tartib bo'lishining o'zi yetmaydi — u noyob bo'lishi kerak. Bir
yilda minglab film chiqadi, va yili bir xil filmlar orasidagi tartibni baza
kafolatlamaydi. Sinovda 50 000 filmli PostgreSQL 16 bazasida faqat
ordering = ["-year"] bilan birinchi 60 sahifa varaqlanganda 263 ta film
ikki marta chiqdi, ko'plari esa umuman ko'rinmadi. Oxiriga noyob "id"
qo'shilgach — bitta ham takror yo'q.
Xuddi shu muammo mijoz so'ragan tartibda ham bor: ?ordering=-rating da
reytingi bir xil filmlar juda ko'p, DRF'ning OrderingFilteri esa oxiriga
noyob kalit qo'shmaydi — sinovda 60 sahifada 340 ta takror chiqdi. Shuning
uchun uni kichik voris klass bilan almashtiramiz:
# apps/core/filters.py
from rest_framework.filters import OrderingFilter
class StableOrderingFilter(OrderingFilter):
"""?ordering=-rating ga oxirgi kalit sifatida "id" qo'shadi.
Reytingi bir xil filmlar ko'p — ular orasidagi tartibni baza kafolatlamaydi
va sahifalar orasida yozuvlar takrorlanib yoki tushib qolishi mumkin.
Noyob "id" tartibni har safar bir xil qiladi.
"""
def get_ordering(self, request, queryset, view):
ordering = super().get_ordering(request, queryset, view)
if ordering and not {"id", "-id", "pk", "-pk"} & set(ordering):
ordering = [*ordering, "id"]
return ordering
Bu klass bilan ?ordering=-rating SQL'da ORDER BY rating DESC, id ASC
bo'lib ketadi va sahifalar orasida takror qolmaydi (apps/core/ ichida
bo'sh __init__.py bo'lishi kerak).
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
# aniq moslik filtrlari — filterset_fields'dagi ro'yxat shu yerga ko'chadi
fields = ["genre__slug", "year"]
# apps/movies/views.py — filterset_fields o'rniga:
filterset_class = MovieFilter
filterset_class berilganda view'dagi filterset_fields ishlatilmaydi.
Shuning uchun aniq moslik filtrlari Meta.fields ga ko'chirilishi kerak:
"year" tushib qolsa, ?year=2019 jimgina e'tiborsiz qoladi — sinovda
shunday bo'ldi.
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.
Bizning serializer har film uchun janr slug'ini chiqaradi: select_related("genre")
bilan sahifa 2 ta SQL so'rov bilan tayyor bo'ladi (sanash va ro'yxat), usiz
esa 22 ta — har film janri uchun alohida so'rov. 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 noyob default ordering — sahifalar
sakramasligi uchun (oxirida doim id). 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.