Asosiy kontentga o‘tish

Maqolalar 10 daqiqa 204 ko‘rish

Backend'ni mobil ilovaga moslash: versiyalash, payload va offline

Mobil mijoz — brauzer emas: eski versiyasi yillab yashaydi, tarmog'i uziladi, har ortiqcha so'rov batareya yeydi. API versiyalash, ekranga mos payload, ETag kesh, delta-sinxronizatsiya va uch tilni bitta javobda berish — Metro tajribasi.

mobile api performance django

Mundarija

Seriyaning avvalgi qismlarida Metro backend'ining bloklarini qurdik. Yakuniy qism — ularni birlashtiruvchi savol: mobil mijozga mos API qanday bo'ladi? Chunki mobil ilova brauzer emas, uning o'z qonunlari bor:

  • foydalanuvchi ilovani yangilamasligi mumkin — bir yil oldingi versiya bugun ham production'ga so'rov yuboradi;

  • tarmoq sekin va uzilib turadi — metroda ayniqsa;

  • har ortiqcha so'rov — batareya va trafik;
  • xato yuz bersa foydalanuvchi "qayta yuklash" tugmasini emas, ilovani o'chirib qo'yishni tanlaydi.

1-qoida: versiya URL'da, buzuvchi o'zgarish — yangi versiyada#

Saytda xato qildingiz — deploy qilasiz, bir daqiqada hammada yangi kod. Mobilda esa yangi versiya App Store tekshiruvidan o'tishi va foydalanuvchi uni o'rnatishi kerak. Siz maydon nomini o'zgartirgan kuni eski ilovalar sinadi — va ularni siz tuzata olmaysiz.

Python
# config/urls.py
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path("admin/", admin.site.urls),
    # versiya — yo'lning birinchi bo'g'ini. v1 hech qachon buzilmaydi:
    # buzuvchi o'zgarish kerak bo'lsa, v2 ochiladi, v1 yashashda davom etadi
    path("api/v1/", include("apps.api_v1.urls")),
]

apps/api_v1 — alohida model'lari yo'q, faqat v1 manzillarini bir joyga yig'adigan paket (mkdir apps/api_v1 && touch apps/api_v1/__init__.py). Oldingi qismlarda config/urls.py da turgan api/ qatorlari shu yerga ko'chadi:

Python
# apps/api_v1/urls.py
from django.urls import include, path

urlpatterns = [
    path("", include("apps.metro.urls")),
    path("", include("apps.notifications.urls")),
    path("", include("apps.accounts.urls")),
]

Mobil ilovadagi manzillar ham /api/... dan /api/v1/... ga o'zgaradi — buni birinchi reliz'dan oldin qilish kerak, keyin kech bo'ladi. Testlardagi manzillarni ham yangilang: masalan, 4-qism testlaridagi /api/auth/google/ endi /api/v1/auth/google/, aks holda ular 404 oladi.

Qoidalar sodda:

  • qo'shish mumkin: yangi maydon, yangi endpoint — eski mijozlar ularni ko'rmaydi, buzilmaydi;

  • mumkin emas: maydonni o'chirish, nomini yoki tipini o'zgartirish, majburiy parametr qo'shish — bularning har biri yangi versiya degani;

  • eski versiyani o'chirishdan oldin uning trafigini o'lchang (access log yoki metrika) — nol bo'lmaguncha o'chirmang.

Nega header emas, URL

Versiyani Accept header'da berish "akademik to'g'ri" hisoblanadi, lekin URL versiyasi log'da ko'rinadi, curl bilan tekshiriladi, mobil jamoaga tushuntirish oson. Amaliyotda soddalik yutadi — Metro'da ham /api/v1/.

2-qoida: bitta ekran — bitta so'rov#

Web'da sahifa 5 ta so'rov qilsa sezilmaydi. Mobilda har so'rov — alohida TLS-qo'l berish, radiouyg'onish, kechikish. Metro'ning bosh ekrani liniyalar va stansiyalarni ko'rsatadi — ularni ikki endpoint qilib bermaymiz, ekranga mos bitta javob qilamiz:

Python
# apps/metro/serializers.py
from rest_framework import serializers

from .models import Line, Station


class StationNestedSerializer(serializers.ModelSerializer):
    class Meta:
        model = Station
        fields = ["id", "name", "status"]


class LineWithStationsSerializer(serializers.ModelSerializer):
    stations = StationNestedSerializer(many=True, read_only=True)

    class Meta:
        model = Line
        fields = ["id", "name", "color", "stations"]
Python
# apps/metro/views.py
from rest_framework.generics import ListAPIView

from .models import Line
from .serializers import LineWithStationsSerializer


class NetworkView(ListAPIView):
    """Bosh ekran uchun butun tarmoq: liniyalar + ichida stansiyalari."""

    serializer_class = LineWithStationsSerializer
    # prefetch bo'lmasa har liniya uchun alohida so'rov ketadi — N+1
    queryset = Line.objects.prefetch_related("stations").order_by("id")
Python
# apps/metro/urls.py — 1-qismdagi ro'yxatga
    path("network/", NetworkView.as_view()),

Ichma-ich serializer N+1 muammosining klassik manbai — prefetch_related esdan chiqsa, 4 liniya 5 ta SQL so'rov bo'ladi. Bu tuzoqni alohida yoritganman: Django ORM va N+1.

3-qoida: o'zgarmagan javobni qayta yubormang — ETag#

Metro tarmog'i haftalab o'zgarmaydi, lekin ilova har ochilganda so'raydi. ConditionalGetMiddleware bilan Django javobga ETag qo'shadi va mijoz If-None-Match yuborsa, o'zgarmagan javobga 304 (bo'sh tana) qaytaradi. Javoblarni siqish uchun yoniga GZipMiddleware qo'yamiz — lekin ikkalasining joyi muhim:

Python
# config/settings.py
MIDDLEWARE = [
    # ro'yxat BOSHIDA: javob tanasini hamma tayyorlab bo'lgach, eng oxirida siqadi
    "django.middleware.gzip.GZipMiddleware",
    "django.middleware.security.SecurityMiddleware",
    # ...
    "django.middleware.http.ConditionalGetMiddleware",  # ETag/304 avtomatik
]

Django javobni middleware ro'yxati bo'ylab pastdan yuqoriga qaytaradi. GZipMiddleware pastda tursa, siqish ETag hisoblanishidan oldin bo'ladi. Django 4.2 dan beri esa gzip BREACH hujumiga qarshi har javobga tasodifiy baytlar qo'shadi — siqilgan tana har safar boshqacha, demak ETag ham har so'rovda yangi va 304 hech qachon qaytmaydi. Sinovda aynan shunday bo'ldi: teskari tartibda gzip so'ragan mijoz ikki ketma-ket so'rovda ikki xil ETag oldi va If-None-Match ga 200 qaytdi. Bu xato gzip so'ramaydigan oddiy curl da ko'rinmaydi, mobil kutubxonalar esa gzip'ni doim so'raydi. To'g'ri tartibda ETag siqilmagan tanadan hisoblanadi, gzip uni "zaif" (W/) deb belgilaydi va 304 ishlaydi.

Terminal
# birinchi so'rov: mobil mijozdek gzip so'raymiz — to'liq javob + ETag
curl -s -o /dev/null -D - -H "Accept-Encoding: gzip" \
  https://api.misol.uz/api/v1/network/ | grep -i etag
# ETag: W/"2de59cffffc80ec7293019e9a02c8b1e"

# ikkinchi so'rov: mijoz ETag'ni qaytaradi — javob 304, tana bo'sh
curl -s -o /dev/null -D - -H "Accept-Encoding: gzip" \
  -H 'If-None-Match: W/"2de59cffffc80ec7293019e9a02c8b1e"' \
  https://api.misol.uz/api/v1/network/ | head -1
# HTTP/1.1 304 Not Modified

Bu ikki qatorlik middleware metro kabi "o'qish ko'p, yozish kam" API'da trafikning katta qismini yo'q qiladi. Server tomonda hisoblash baribir ketadi — uni ham kesish kerak bo'lsa, keyingi bosqich kesh: Redis keshlash strategiyalari.

4-qoida: offline — xato holati emas, normal holat#

Metro ilovasining qiziq sharti: foydalanuvchi aynan yer ostida, aloqasiz paytda ilovaga eng ko'p qaraydi. Yechim — ma'lumotni qurilmada saqlash va tarmoq borida delta-sinxronizatsiya:

Python
# apps/metro/views.py
from django.utils.dateparse import parse_datetime
from rest_framework.exceptions import ValidationError
from rest_framework.generics import ListAPIView

from .models import Station
from .serializers import StationSyncSerializer


class StationSyncView(ListAPIView):
    """?updated_after=2026-08-01T00:00:00Z — faqat o'shandan keyin o'zgarganlar.

    Ilova birinchi ochilishda hammasini oladi va lokal bazaga yozadi,
    keyin faqat farqni so'raydi — trafik ham, vaqt ham minimal.
    """

    serializer_class = StationSyncSerializer

    def get_queryset(self):
        qs = Station.objects.order_by("updated_at")
        raw = self.request.query_params.get("updated_after")
        if not raw:
            return qs
        moment = parse_datetime(raw)
        if moment is None:
            raise ValidationError({"updated_after": "ISO 8601 formatida bo'lsin"})
        return qs.filter(updated_at__gt=moment)
Python
# apps/metro/urls.py — 1-qismdagi ro'yxatga
    path("stations/sync/", StationSyncView.as_view()),

Buning ishlashi uchun modelda updated_at = models.DateTimeField(auto_now=True) bo'lishi kerak — va o'chirilgan yozuvlar uchun qattiq o'chirish o'rniga is_deleted bayrog'i (aks holda mijoz o'chirilganni bilmay qoladi). StationSyncSerializer — oddiy ModelSerializer, fields ichida id, updated_at va is_deleted albatta bo'lsin.

Mijoz tomonda ikki nozik joy bor:

  • updated_after ga telefon soatini emas, oldingi javobda kelgan eng katta updated_at ni yuboring. Telefon soati bir necha daqiqa oldinda bo'lsa, shu daqiqalarda o'zgargan stansiyalar hech qachon yetib kelmaydi;

  • vaqtni URL'ga kodlab qo'ying. 2026-08-01T05:00:00+05:00 dagi + query satrida bo'sh joyga aylanadi va endpoint to'g'ri sanaga ham 400 qaytaradi. Z bilan tugaydigan UTC vaqt yoki %2B bilan kodlangan + muammosiz o'tadi.

Takroriy so'rovga chidamlilik (idempotentlik)

Mobil tarmoqda so'rov ketdi-yu, javob yo'qoldi — ilova qayta uradi. POST endpoint bunga tayyor bo'lmasa, bitta amal ikki marta bajariladi. Retseptlar: yaratishda update_or_create (3-qismdagi token ro'yxati shunday ishlaydi), pul/kritik amallarda mijoz yuboradigan Idempotency-Key header va uni server tomonda eslab qolish.

5-qoida: uch til — bitta javobda#

Metro kontenti uch tilda (uz/ru/en). Klassik yechim — Accept-Language bo'yicha bitta tilni qaytarish. Metro'da esa boshqa qaror qabul qilingan: hamma til bitta JSONField'da, javob ham shunday ketadi:

JSON
{"id": 4, "name": {"uz": "Amir Temur xiyoboni", "ru": "Сквер Амира Темура", "en": "Amir Temur Square"}}

Sabablari amaliy:

  • foydalanuvchi tilni ilova ichida, offline holda almashtira oladi — server so'rovsiz;

  • kesh va ETag samaradorligi: javob tilga bog'liq emas, bitta variant;

  • uch qisqa satr javob hajmini sezilarli oshirmaydi. Kontent hajmi cheklangan ma'lumotnoma bo'lgani uchun bu qaror o'zini oqlaydi; uzun matnlar bilan ishlaydigan tizimda esa boshqacha yo'l tanlangan bo'lardi.

Xato javoblari ham API'ning qismi

Ilova xatoni foydalanuvchi tilida ko'rsatishi kerak. Matn yuborish o'rniga kod yuboring — {"error": {"code": "station_closed"}} — tarjimasi ilovada. Matn yuborsangiz, xabarni o'zgartirish uchun yana App Store navbatiga turasiz.

Xulosa — mobil API dizaynining besh sinovi#

Har yangi endpoint'ni shu savollar bilan tekshiring:

  1. Bir yil oldingi ilova versiyasi bu javobni ko'rsa sinadimi? (versiyalash)
  2. Ekran nechta so'rov qilyapti? (payload dizayni, N+1)
  3. O'zgarmagan ma'lumot qayta yuborilyaptimi? (ETag/304, kesh)
  4. So'rov ikki marta kelsa nima bo'ladi? (idempotentlik)
  5. Javob aloqasiz metro vagonida ham ma'noli ishlaydimi? (delta-sync, offline)

Shu bilan Metro backend amaliyoti seriyasi yakunlandi: geo-so'rovlar, real-time, push, autentifikatsiya va mobil dizayn — birgalikda Play Market'da 100K+ yuklab olingan ilovaning to'liq backend skeleti. Loyihaning o'zi haqida: Tashkent Metro case-study.

10 daqiqa 204 marta o‘qildi