Django 5.2: Wlasne Middleware i Obsluga Sygnalow -- Przygotowanie do Rozmow Rekrutacyjnych

Praktyczny przewodnik po tworzeniu wlasnych middleware i obsludze sygnalow w Django 5.2. Struktura middleware, asynchroniczne middleware, sygnaly post_save i pre_save, wlasne sygnaly domenowe oraz najczesciej zadawane pytania rekrutacyjne.

Diagram architektury middleware i sygnalow w Django 5.2 z przeplywem requestow i zdarzeniami domenowymi

Django 5.2 LTS wprowadza szereg usprawnien w zakresie asynchronicznego przetwarzania i obslugi zdarzen, ktore bezposrednio wplywaja na sposob projektowania warstwy middleware oraz implementacji sygnalow. Middleware i sygnaly stanowia dwa fundamentalne mechanizmy frameworka, ktore pojawiaja sie na niemal kazdej rozmowie kwalifikacyjnej dotyczacej Django. Middleware odpowiada za przetwarzanie requestow i response'ow na poziomie globalnym, natomiast sygnaly umozliwiaja luznie powiazanym komponentom aplikacji reagowanie na zdarzenia wewnetrzne. Niniejszy artykul omawia oba mechanizmy od podstaw, prezentuje produkcyjne przyklady kodu i przygotowuje do pytan rekrutacyjnych z zakresu Django.

Scigawka rekrutacyjna: Middleware vs Sygnaly

Middleware przechwytuje kazdy request HTTP przechodzacy przez aplikacje -- idealny do logowania, uwierzytelniania, modyfikacji naglowkow. Wykonuje sie w scisle okreslonej kolejnosci zdefiniowanej w MIDDLEWARE.

Sygnaly reaguja na zdarzenia wewnetrzne modelu (zapis, usuwanie) lub zdarzenia domenowe -- idealny do efektow ubocznych takich jak wysylka emaili, tworzenie powiazanych obiektow, walidacja danych. Dzialaja w modelu publish-subscribe.

Jak dziala middleware w Django

Middleware w Django to klasa implementujaca wzorzec dekoratora wokol cyklu request/response. Kazda klasa middleware otrzymuje callable get_response, ktory reprezentuje nastepna warstwe w lancuchu -- moze to byc kolejne middleware lub sam widok. Wywolanie get_response(request) przekazuje request w dol lancucha i zwraca obiekt response.

Podstawowa struktura middleware wyglada nastepujaco:

python
# middleware.py - Basic middleware structure
class SimpleMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response
        # One-time configuration happens here at server start

    def __call__(self, request):
        # Code executed BEFORE the view (and later middleware)
        response = self.get_response(request)
        # Code executed AFTER the view (on the way back)
        return response

Metoda __init__ jest wywolywana jednokrotnie przy starcie serwera. To odpowiednie miejsce na inicjalizacje zasobow, ktore maja byc wspoldzielone miedzy requestami -- na przyklad konfiguracja loggera, ladowanie ustawien z bazy czy inicjalizacja klienta cache. Metoda __call__ jest wywolywana przy kazdym requescie i to tutaj znajduje sie wlasciwa logika middleware.

Warto zwrocic uwage na symetryczna nature tego wzorca: kod przed wywolaniem get_response wykonuje sie podczas fazy request (w dol lancucha), a kod po wywolaniu -- podczas fazy response (w gore lancucha). Ta dwukierunkowa architektura umozliwia middleware jednoczesne modyfikowanie zarowno requestu, jak i response'u.

Budowanie middleware do logowania requestow

Jednym z najczestszych zastosowan wlasnego middleware jest logowanie requestow HTTP. Ponizszy przyklad rejestruje metode, sciezke, kod statusu, czas trwania i adres IP kazdego requestu. Dodatkowo implementuje metode process_exception, ktora przechwytuje wyjatki rzucone przez widoki:

python
# apps/core/middleware.py
import logging
import time

logger = logging.getLogger('django.request')


class RequestLoggingMiddleware:
    def __init__(self, get_response):
        self.get_response = get_response

    def __call__(self, request):
        start_time = time.monotonic()
        # Attach metadata before view processing
        request.start_time = start_time

        response = self.get_response(request)

        duration = time.monotonic() - start_time
        logger.info(
            'method=%s path=%s status=%d duration=%.3fs ip=%s',
            request.method,
            request.get_full_path(),
            response.status_code,
            duration,
            request.META.get('REMOTE_ADDR'),
        )
        return response

    def process_exception(self, request, exception):
        # Called only when the view raises an exception
        duration = time.monotonic() - getattr(request, 'start_time', 0)
        logger.error(
            'method=%s path=%s exception=%s duration=%.3fs',
            request.method,
            request.get_full_path(),
            str(exception),
            duration,
        )
        return None  # Let Django's default exception handling continue

Uzycie time.monotonic() zamiast time.time() jest swiadoma decyzja -- zegar monotoniczny nie podlega korektom systemowym (np. synchronizacji NTP), co gwarantuje dokladny pomiar czasu trwania requestu nawet w przypadku korekty zegara systemowego.

Metoda process_exception jest jednym z hookow middleware Django. Zwracanie None oznacza, ze middleware nie obsluguje wyjatku samodzielnie i przekazuje go do domyslnego mechanizmu obslugi bledow Django. Gdyby metoda zwrocila obiekt HttpResponse, Django uzylby tego response'u zamiast standardowej strony bledu.

Kolejnosc middleware i ustawienie MIDDLEWARE

Kolejnosc klas w liscie MIDDLEWARE ma kluczowe znaczenie. Django przetwarza middleware od gory do dolu podczas fazy request i od dolu do gory podczas fazy response. Oznacza to, ze middleware umieszczone wyzej na liscie "widzi" request jako pierwsze, ale response jako ostatnie.

python
# settings.py - Middleware ordering matters
MIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    # Custom middleware placed after Django's core stack
    'apps.core.middleware.RequestLoggingMiddleware',
]

Umieszczenie RequestLoggingMiddleware na koncu listy oznacza, ze podczas fazy request bedzie ono wykonywane jako ostatnie (tuz przed widokiem), a podczas fazy response -- jako pierwsze (zaraz po widoku). Dzieki temu pomiar czasu obejmuje wylacznie czas przetwarzania widoku, bez narzutu pozostalych warstw middleware.

Gdyby natomiast middleware logujace zostalo umieszczone na poczatku listy (przed SecurityMiddleware), mierzyloby calkowity czas przetwarzania lancucha, wlacznie z nakladem wszystkich pozostalych middleware. Wybor pozycji zalezy od tego, co dokladnie ma byc mierzone.

Typowym bledem rekrutacyjnym jest umieszczenie wlasnego middleware przed AuthenticationMiddleware, gdy logika middleware wymaga dostepu do request.user. W takiej konfiguracji obiekt uzytkownika nie bedzie jeszcze dostepny.

Asynchroniczne middleware w Django 5.2

Django 5.2 rozszerza wsparcie dla asynchronicznego przetwarzania middleware. Atrybuty async_capable i sync_capable informuja framework, czy middleware moze dzialac w kontekscie asynchronicznym. Ustawienie sync_capable = False wymusza, aby Django wywolywalo middleware wylacznie w trybie async:

python
# apps/core/middleware.py
import asyncio
import time


class AsyncTimingMiddleware:
    # Mark this middleware as async-capable
    async_capable = True
    sync_capable = False

    def __init__(self, get_response):
        self.get_response = get_response

    async def __call__(self, request):
        start = time.monotonic()
        # get_response is awaitable in async context
        response = await self.get_response(request)
        duration = time.monotonic() - start
        response['X-Request-Duration'] = f'{duration:.4f}s'
        return response

W kontekscie asynchronicznym get_response jest obiektem awaitable, dlatego nalezy uzyc slowa kluczowego await. Middleware dodaje naglowek X-Request-Duration do kazdego response'u, co jest przydatne do monitorowania wydajnosci po stronie klienta lub systemow APM.

Nalezy pamietac, ze pelne korzysci z asynchronicznego middleware uzyskuje sie dopiero w polaczeniu z asynchronicznymi widokami i serwerem ASGI (np. Daphne, Uvicorn). W srodowisku WSGI Django automatycznie opakowuje asynchroniczne middleware w synchroniczny wrapper, co neguje potencjalne zyski wydajnosciowe.

Gotowy na rozmowy o Django?

Ćwicz z naszymi interaktywnymi symulatorami, flashcards i testami technicznymi.

Sygnaly Django -- wzorzec publish-subscribe

Sygnaly w Django implementuja wzorzec publish-subscribe (obserwator), umozliwiajac luznie powiazanym komponentom aplikacji reagowanie na okreslone zdarzenia. Framework dostarcza zestaw wbudowanych sygnalow (pre_save, post_save, pre_delete, post_delete, m2m_changed, request_started, request_finished) oraz pozwala definiowac wlasne sygnaly domenowe.

Glowna zaleta sygnalow jest decouplowanie -- nadawca sygnalu nie musi wiedziec, kto na niego nasluchuje. Dzieki temu mozna dodawac nowe reakcje na zdarzenia bez modyfikacji istniejacego kodu. To szczegolnie wartosciowe w duzych projektach, gdzie rozne aplikacje Django musza reagowac na te same zdarzenia.

post_save do automatycznego tworzenia profilu

Jednym z najczestszych zastosowan sygnalu post_save jest automatyczne tworzenie powiazanych obiektow po zapisaniu modelu. Klasyczny przyklad to tworzenie profilu uzytkownika zaraz po rejestracji nowego konta:

python
# apps/accounts/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model
from apps.accounts.models import UserProfile

User = get_user_model()


@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
    # 'created' is True only on INSERT, False on UPDATE
    if created:
        UserProfile.objects.create(
            user=instance,
            display_name=instance.get_full_name() or instance.username,
        )

Dekorator @receiver rejestruje funkcje jako odbiorce sygnalu post_save wysylanego przez model User. Parametr created pozwala odroznic tworzenie nowego rekordu (INSERT) od aktualizacji istniejacego (UPDATE). Bez tego sprawdzenia profil bylby tworzony przy kazdym zapisie uzytkownika, co prowadziloby do bledu IntegrityError z powodu naruszenia ograniczenia unikalnosci.

Parametr **kwargs jest obowiazkowy w sygnaturze odbiorcy. Django moze przekazywac dodatkowe argumenty w przyszlych wersjach, a ich brak w sygnaturze spowodowalby blad w runtime.

pre_save do walidacji i transformacji danych

Sygnal pre_save jest wysylany tuz przed zapisaniem obiektu do bazy danych, co czyni go idealnym miejscem na walidacje i automatyczne transformacje. Ponizszy przyklad automatycznie generuje unikalne sluga na podstawie tytulu artykulu:

python
# apps/blog/signals.py
from django.db.models.signals import pre_save
from django.dispatch import receiver
from django.utils.text import slugify
from apps.blog.models import Article


@receiver(pre_save, sender=Article)
def auto_generate_slug(sender, instance, **kwargs):
    if not instance.slug:
        base_slug = slugify(instance.title)
        slug = base_slug
        counter = 1
        # Ensure slug uniqueness
        while Article.objects.filter(slug=slug).exclude(pk=instance.pk).exists():
            slug = f'{base_slug}-{counter}'
            counter += 1
        instance.slug = slug

Logika generowania sluga sprawdza unikalnosc w petli, dodajac sufiks numeryczny w przypadku kolizji. Uzycie exclude(pk=instance.pk) zapobiega falszywie pozytywnym kolizjom podczas aktualizacji istniejacego artykulu -- bez tego wykluczenia obiekt zgloszowalby kolizje sam ze soba.

Warto podkreslic, ze modyfikacja instance.slug w sygnale pre_save jest skuteczna, poniewaz Django uzywa zmienionego obiektu do wykonania zapytania SQL. To rozni pre_save od post_save, gdzie modyfikacja instancji wymaga dodatkowego wywolania save(), co prowadzi do niebezpiecznej rekursji.

Rejestracja sygnalow w AppConfig.ready()

Same definicje odbiorcow sygnalow nie wystarczaja -- musza byc zaimportowane, aby Python je zarejestrowal. Zalecanym miejscem na import modulu sygnalow jest metoda ready() klasy AppConfig:

python
# apps/accounts/apps.py
from django.apps import AppConfig


class AccountsConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'apps.accounts'

    def ready(self):
        # Import signals module to register receivers
        import apps.accounts.signals  # noqa: F401

Metoda ready() jest wywolywana jednokrotnie po zaladowaniu wszystkich aplikacji Django. Import modulu sygnalow w tym miejscu gwarantuje, ze odbiory zostana zarejestrowane dokladnie raz, niezaleznie od liczby workerow czy watkow.

Komentarz # noqa: F401 tlumaczy linterowi, ze import bez uzycia jest celowy -- modul importowany jest wylacznie w celu wywolania efektow ubocznych (rejestracji dekoratorow @receiver). Pominiecnie tego importu jest jednym z najczestszych bledow poczatkujacych -- sygnaly sa poprawnie zdefiniowane, ale nigdy nie zostaja zarejestrowane.

Budowanie wlasnych sygnalow dla zdarzen domenowych

Oproc wbudowanych sygnalow modelowych Django pozwala definiowac wlasne sygnaly reprezentujace zdarzenia biznesowe. Jest to szczegolnie przydatne w architekturze opartej na zdarzeniach (event-driven), gdzie rozne podsystemy musza reagowac na te same zdarzenia domenowe bez bezposredniego powiazania:

python
# apps/orders/signals.py
from django.dispatch import Signal

# Define custom signals with documentation
order_completed = Signal()  # Sent after payment confirmation
order_refunded = Signal()   # Sent after refund processing


# apps/orders/services.py
from apps.orders.signals import order_completed


def complete_order(order):
    order.status = 'completed'
    order.save()
    # Dispatch signal with relevant context
    order_completed.send(
        sender=order.__class__,
        order=order,
        total=order.total_amount,
    )


# apps/notifications/receivers.py
from django.dispatch import receiver
from apps.orders.signals import order_completed


@receiver(order_completed)
def send_order_confirmation_email(sender, order, **kwargs):
    # Email logic decoupled from order processing
    from apps.notifications.services import send_email
    send_email(
        to=order.customer.email,
        template='order_confirmation',
        context={'order': order},
    )

W tym wzorcu modul orders nie importuje niczego z modulu notifications. Jedyna zaleznosc biegnie w przeciwnym kierunku: notifications importuje sygnal z orders. Dzieki temu mozna dodawac nowe reakcje na zdarzenie order_completed (np. aktualizacja statystyk, powiadomienie magazynu, rejestracja w systemie analitycznym) bez jakiejkolwiek modyfikacji logiki zamowien.

Metoda send() jest synchroniczna i wywoluje wszystkich odbiorcow sekwencyjnie w tym samym watku. Dla operacji czasochlonnych (np. wysylka emaili) zaleca sie uzycie kolejki zadan (Celery, Django-Q2) wewnatrz odbiorcy, aby nie blokowac glownego cyklu request/response.

Najczestsze pulapki rekrutacyjne

Podczas rozmow kwalifikacyjnych dotyczacych Django middleware i sygnalow regularnie pojawiaja sie pytania o subtelne zachowania, ktore prowadza do trudnych do zdiagnozowania bledow produkcyjnych.

QuerySet.update() omija sygnaly

Metody QuerySet.update() i QuerySet.delete() operuja bezposrednio na poziomie SQL i nie wywoluja sygnalow pre_save/post_save ani pre_delete/post_delete. Takze operacje bulk_create() domyslnie pomijaja sygnaly. Jesli logika biznesowa opiera sie na sygnalach, operacje masowe moga prowadzic do niespojnosci danych. Rozwiazaniem jest jawne iterowanie po obiektach i wywolywanie save() lub delete() na kazdym z nich, badz manualne emitowanie sygnalow po operacji masowej.

Django 5.2 LTS -- wsparcie dlugookresowe

Django 5.2 jest wersja LTS (Long-Term Support), co oznacza, ze bedzie otrzymywac poprawki bezpieczenstwa do kwietnia 2028 roku. To sprawia, ze jest to preferowana wersja do projektow produkcyjnych. Wsparcie dla asynchronicznego middleware i widokow zostalo znaczaco rozszerzone w tej wersji, a stabilnosc API sygnalow pozostaje niezmieniona od wielu lat.

Kolejne czeste pytanie dotyczy problemu rekursji w sygnalach. Jesli odbiorca post_save wywoluje save() na tym samym obiekcie, prowadzi to do nieskonczonej petli. Standardowym rozwiazaniem jest uzycie update() na QuerySet zamiast save() na instancji, lub zastosowanie flagi ochronnej na obiekcie (instance._skip_signal = True) sprawdzanej na poczatku odbiorcy.

Warto rowniez wiedziec, ze sygnaly Django sa synchroniczne i wykonywane w ramach tej samej transakcji bazodanowej co operacja, ktora je wywolala. Oznacza to, ze blad w odbiorsygnalu moze cofnac cala transakcje. Jesli efekt uboczny nie powinien wplywac na powodzenie glownej operacji, nalezy uzyc transaction.on_commit() do odlozenia wykonania lub obsluzyc wyjatki wewnatrz odbiorcy.

Porownanie Middleware i Sygnalow

| Aspekt | Middleware | Sygnaly | |---|---|---| | Zakres dzialania | Request/Response HTTP | Zdarzenia wewnetrzne (model, request, domenowe) | | Moment wykonania | Przy kazdym requescie HTTP | Przy zapisie, usuwaniu, lub zdarzeniu niestandardowym | | Kolejnosc | Scisle zdefiniowana w MIDDLEWARE | Nieokreslona (nie nalezy polegac na kolejnosci) | | Konfiguracja | settings.py lista MIDDLEWARE | Dekorator @receiver + import w AppConfig.ready() | | Asynchronicznosc | async_capable = True od Django 5.2 | Synchroniczne (uzywac Celery dla zadan async) | | Typowe zastosowania | Logowanie, CORS, bezpieczenstwo, cache | Tworzenie powiazanych obiektow, walidacja, powiadomienia | | Testowanie | Testy integracyjne HTTP | Testy jednostkowe z Signal.send() |

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Podsumowanie

Middleware i sygnaly stanowia dwa komplementarne mechanizmy rozszerzania zachowania aplikacji Django. Kluczowe wnioski do zapamietania przed rozmowa rekrutacyjna:

  • Middleware przetwarza kazdy request HTTP w scisle okreslonej kolejnosci -- kod przed get_response wykonuje sie w fazie request, kod po nim w fazie response
  • Kolejnosc w liscie MIDDLEWARE determinuje zachowanie aplikacji -- wlasne middleware powinno byc umieszczone po wbudowanych warstwach Django, od ktorych zalezy
  • Asynchroniczne middleware w Django 5.2 wymaga atrybutow async_capable = True i serwera ASGI do pelnego wykorzystania
  • Sygnaly pre_save i post_save umozliwiaja automatyczna walidacje, transformacje i tworzenie powiazanych obiektow bez modyfikacji logiki widokow
  • Rejestracja sygnalow w AppConfig.ready() jest obowiazkowa -- bez importu modulu sygnalow odbiory nigdy nie zostana aktywowane
  • Operacje masowe (QuerySet.update(), bulk_create()) omijaja sygnaly -- jest to najczestsza przyczyna niespojnosci danych w aplikacjach produkcyjnych
  • Wlasne sygnaly domenowe pozwalaja budowac architekture event-driven z luznie powiazanymi komponentami
  • Rekursja w post_save to klasyczna pulapka -- nalezy unikac wywolywania save() na tym samym obiekcie wewnatrz odbiorcy tego sygnalu

Zacznij ćwiczyć!

Sprawdź swoją wiedzę z naszymi symulatorami rozmów i testami technicznymi.

Tagi

#django
#python
#middleware
#signals
#interview

Udostępnij

Powiązane artykuły