Django 5.2: Custom Middleware e Gestione dei Segnali per Colloqui Tecnici

Padroneggiare middleware e segnali in Django 5.2: pipeline middleware, middleware asincrono, pre_save/post_save signals e pattern comuni nei colloqui tecnici.

Tutorial su custom middleware e gestione segnali in Django 5.2

Django 5.2 rappresenta una release LTS (Long Term Support) che consolida il sistema dei middleware e dei segnali con miglioramenti significativi, in particolare il supporto completo alla modalità asincrona. Nei colloqui tecnici per posizioni backend Python, la padronanza di questi due meccanismi distingue i candidati che comprendono l'architettura di Django da quelli che si limitano a utilizzarne le funzionalità superficiali. Il middleware controlla il flusso delle richieste HTTP a livello globale, mentre i segnali implementano un pattern publish-subscribe che disaccoppia la logica di business tra componenti indipendenti.

Questo articolo analizza in profondità entrambi i meccanismi, dalla struttura fondamentale fino ai pattern avanzati con supporto asincrono introdotto in Django 5.2, fornendo esempi di codice pronti per la produzione e le risposte attese nelle domande di colloquio Django.

Riferimento rapido per il colloquio

Le tre domande ricorrenti nei colloqui tecnici su Django middleware e signals: (1) spiegare il ciclo di vita di una richiesta attraverso lo stack middleware, (2) descrivere quando utilizzare pre_save rispetto a post_save, (3) illustrare perché QuerySet.update() non attiva i segnali del modello. Padroneggiare questi concetti copre oltre il 90% delle domande su middleware e segnali nei colloqui Django.

Come funziona il middleware in Django

Il middleware in Django opera come una catena di componenti che avvolgono la funzione di vista (view). Ogni middleware riceve la richiesta HTTP in ingresso, la elabora o la modifica, la passa al middleware successivo nella catena e infine processa la risposta restituita dalla view. La struttura segue il pattern "onion" (a cipolla): la richiesta attraversa ogni strato dall'esterno verso l'interno, raggiunge la view, e la risposta risale attraverso gli stessi strati in ordine inverso.

A partire da Django 1.10, il middleware utilizza un'interfaccia basata su classi con il metodo __call__. Il costruttore __init__ riceve la funzione get_response, che rappresenta il middleware successivo nella catena oppure la view stessa nel caso dell'ultimo middleware. Questa architettura consente di eseguire logica sia prima che dopo l'elaborazione della vista.

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

Il metodo __init__ viene invocato una sola volta all'avvio del server, rendendolo il punto ideale per operazioni di configurazione costose come l'inizializzazione di connessioni a servizi esterni o il caricamento di file di configurazione. Il metodo __call__ viene invece eseguito ad ogni richiesta HTTP, e tutto il codice precedente alla chiamata self.get_response(request) opera nella fase di richiesta, mentre il codice successivo opera nella fase di risposta.

Costruire un middleware di logging delle richieste

Un caso d'uso classico del middleware, e una domanda frequente nei colloqui tecnici, riguarda il logging strutturato delle richieste HTTP. Il middleware seguente misura la durata di ogni richiesta, registra il metodo HTTP, il percorso, lo status code e l'indirizzo IP del client, e gestisce le eccezioni non catturate tramite il metodo hook process_exception.

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

L'utilizzo di time.monotonic() anziché time.time() garantisce misurazioni corrette anche in presenza di aggiustamenti dell'orologio di sistema (NTP sync). L'attributo start_time viene agganciato direttamente all'oggetto request, una tecnica idiomatica in Django per trasportare metadati tra middleware e view. Il metodo process_exception viene invocato esclusivamente quando la view solleva un'eccezione non gestita; restituendo None, il middleware consente alla gestione standard delle eccezioni di Django di proseguire.

Ordinamento del middleware

L'ordine di dichiarazione dei middleware nella lista MIDDLEWARE del file settings.py determina l'ordine di esecuzione. La richiesta attraversa i middleware dall'alto verso il basso, mentre la risposta li attraversa dal basso verso l'alto. Posizionare un middleware personalizzato nel punto sbagliato della catena costituisce un errore comune che può causare comportamenti inattesi e difficili da diagnosticare.

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',
]

SecurityMiddleware occupa la prima posizione perché deve gestire redirect HTTPS e header di sicurezza prima di qualsiasi altra elaborazione. SessionMiddleware precede AuthenticationMiddleware perché il sistema di autenticazione dipende dalla sessione per identificare l'utente corrente. Un middleware di logging personalizzato viene tipicamente posizionato alla fine della catena, in modo da poter registrare le informazioni sull'utente autenticato e sullo stato della sessione già disponibili.

Nei colloqui tecnici, una domanda tipica chiede di spiegare cosa accade se AuthenticationMiddleware viene posizionato prima di SessionMiddleware: la risposta è che request.user risulterebbe un utente anonimo per tutte le richieste, poiché la sessione non sarebbe ancora stata caricata al momento del tentativo di autenticazione.

Middleware asincrono in Django 5.2

Django 5.2 estende il supporto ai middleware asincroni, consentendo di sfruttare appieno le view asincrone e le operazioni I/O non bloccanti. Un middleware asincrono dichiara async_capable = True e sync_capable = False, e il metodo __call__ diventa una coroutine che utilizza await per invocare get_response.

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

La distinzione tra async_capable e sync_capable è cruciale: impostando sync_capable = False, il middleware comunica a Django che non è in grado di gestire view sincrone. Se una view sincrona attraversa questo middleware, Django eseguirà automaticamente il wrapping in un contesto asincrono tramite sync_to_async. Impostando invece entrambi a True, il middleware deve essere in grado di funzionare in entrambe le modalità. In Django 5.2, questa flessibilità consente di costruire stack middleware ibridi dove componenti sincroni e asincroni coesistono senza interventi manuali.

Pronto a superare i tuoi colloqui su Django?

Pratica con i nostri simulatori interattivi, flashcards e test tecnici.

Segnali in Django: il pattern Publish-Subscribe

I segnali (signals) in Django implementano il pattern publish-subscribe a livello di framework, consentendo a componenti disaccoppiati di reagire a eventi specifici senza dipendenze dirette. A differenza del middleware, che opera a livello di richiesta HTTP, i segnali operano a livello di eventi interni del framework: salvataggio di modelli, cancellazione di record, esecuzione di migrazioni, elaborazione di richieste.

Django fornisce segnali predefiniti per le operazioni più comuni sui modelli: pre_save e post_save (prima e dopo il salvataggio), pre_delete e post_delete (prima e dopo la cancellazione), m2m_changed (modifica di relazioni many-to-many). Ogni segnale trasporta informazioni contestuali specifiche tramite argomenti keyword.

post_save per la creazione automatica del profilo

Il caso d'uso più classico di post_save, e il più frequente nelle domande di colloquio, riguarda la creazione automatica di un profilo utente quando viene registrato un nuovo utente. Il decoratore @receiver collega la funzione handler al segnale post_save filtrato per il modello User.

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,
        )

Il parametro created rappresenta un dettaglio critico: vale True esclusivamente quando l'operazione è un INSERT (creazione di un nuovo record), e False per le operazioni UPDATE. Omettere il controllo su created causerebbe un tentativo di creare un profilo duplicato ad ogni salvataggio dell'utente, generando un'eccezione IntegrityError. Nei colloqui tecnici, questa distinzione viene frequentemente verificata per valutare la comprensione del candidato del ciclo di vita dei segnali Django.

pre_save per validazione e trasformazione

Il segnale pre_save viene emesso prima che Django esegua la query SQL di salvataggio, rendendolo ideale per operazioni di validazione, normalizzazione o trasformazione dei dati. Un esempio classico riguarda la generazione automatica dello slug a partire dal titolo di un articolo.

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

La logica di unicità dello slug merita attenzione: il ciclo while verifica l'esistenza di slug duplicati escludendo il record corrente (tramite exclude(pk=instance.pk)), garantendo che un aggiornamento del record non entri in conflitto con sé stesso. Questo pattern è robusto ma presenta un rischio di race condition in ambienti con alta concorrenza; in produzione, l'aggiunta di un vincolo unique a livello di database con gestione dell'eccezione IntegrityError fornisce una garanzia più solida.

Registrazione dei segnali in AppConfig.ready()

I segnali devono essere registrati durante l'inizializzazione dell'applicazione Django. Il metodo raccomandato consiste nell'importare il modulo dei segnali all'interno del metodo ready() della classe AppConfig. Questo garantisce che i receiver vengano collegati esattamente una volta, al momento dell'avvio dell'applicazione.

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

Il commento # noqa: F401 sopprime l'avvertimento del linter riguardo all'import inutilizzato, poiché l'importazione ha l'effetto collaterale di registrare i decorator @receiver. Un errore comune nei colloqui consiste nel dimenticare questa registrazione: senza l'import nel metodo ready(), i segnali vengono definiti ma mai collegati, e gli handler non vengono mai eseguiti. Django non emette alcun avvertimento in questo caso, rendendo il problema particolarmente insidioso.

Segnali personalizzati per eventi di dominio

Oltre ai segnali predefiniti di Django, il framework consente di definire segnali personalizzati per modellare eventi specifici del dominio applicativo. Questo pattern disaccoppia efficacemente la logica di business: il componente che emette l'evento non conosce i sottoscrittori, e i sottoscrittori possono essere aggiunti o rimossi senza modificare il codice emittente.

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},
    )

La separazione in tre file distinti illustra il principio di disaccoppiamento: signals.py definisce gli eventi, services.py li emette e receivers.py reagisce ad essi. Il modulo degli ordini non ha alcuna conoscenza del sistema di notifica, e il sistema di notifica può essere rimosso o sostituito senza toccare una sola riga di codice nel modulo degli ordini. Nei colloqui, questo pattern dimostra una comprensione matura dell'architettura Django e dei principi SOLID.

Errori comuni nei colloqui tecnici

QuerySet.update() non attiva i segnali

Il metodo QuerySet.update() esegue un UPDATE SQL diretto a livello di database, bypassando completamente il metodo save() del modello e di conseguenza i segnali pre_save e post_save. Lo stesso vale per QuerySet.delete() rispetto a pre_delete e post_delete. Nei colloqui, questa distinzione rappresenta una delle domande trabocchetto più frequenti su Django.

I candidati nei colloqui tecnici Django cadono frequentemente su questi aspetti legati a middleware e segnali:

  • Ordine del middleware invertito: posizionare AuthenticationMiddleware prima di SessionMiddleware impedisce il funzionamento dell'autenticazione basata su sessione
  • Segnali non registrati: definire i receiver senza importare il modulo in AppConfig.ready() produce handler che non vengono mai eseguiti
  • Confusione tra created e update: non verificare il parametro created in post_save causa tentativi di creazione duplicata ad ogni salvataggio
  • Dipendenze circolari nei segnali: un segnale post_save che invoca .save() sullo stesso modello genera una ricorsione infinita
  • Segnali sincroni in contesti asincroni: in Django 5.2, l'invocazione di Signal.send() in una view asincrona può causare blocchi; occorre utilizzare Signal.asend() per la compatibilità asincrona
  • Eccezioni non gestite nei receiver: un'eccezione in un receiver post_save viene propagata al codice chiamante, potenzialmente interrompendo il flusso dell'applicazione; Signal.send_robust() cattura le eccezioni e le restituisce come risultato
Django 5.2 LTS

Django 5.2, rilasciato ad aprile 2025, è una versione Long Term Support con supporto garantito fino ad aprile 2028. Le principali novità includono il supporto completo ai middleware e segnali asincroni (asend, asend_robust), il composite primary key, le migrazioni automatiche e i miglioramenti alle performance del query compiler. Per la preparazione ai colloqui tecnici, concentrarsi sulle funzionalità LTS garantisce di studiare una versione con rilevanza a lungo termine.

Middleware vs Segnali: tabella comparativa

La scelta tra middleware e segnali dipende dal livello di astrazione e dal tipo di evento da gestire. La tabella seguente sintetizza le differenze fondamentali.

| Aspetto | Middleware | Signals | |---|---|---| | Modifica request/response | Sì | No | | Autenticazione e autorizzazione | Sì | No | | Hook del ciclo di vita del modello | No | Sì | | Aspetti trasversali (logging, timing) | Sì | Possibile ma non ideale | | Logica di business disaccoppiata | No | Sì | | Supporto async in Django 5.2 | Completo | Completo (asend/asend_robust) | | Ambito di esecuzione | Ogni richiesta HTTP | Eventi specifici (save, delete, custom) |

La regola pratica: il middleware gestisce aspetti trasversali a livello di richiesta HTTP (sicurezza, logging, autenticazione, compressione), mentre i segnali gestiscono reazioni a eventi interni del framework (creazione di record, modifica di stato, eventi di dominio). Utilizzare un middleware per reagire al salvataggio di un modello, o un segnale per modificare una risposta HTTP, rappresenta un antipattern architetturale.

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Conclusione

Il middleware e i segnali costituiscono due pilastri architetturali di Django che operano a livelli distinti ma complementari. I punti chiave per i colloqui tecnici si possono riassumere come segue:

  • Il middleware opera a livello di richiesta HTTP seguendo il pattern a cipolla: la richiesta attraversa lo stack dall'alto verso il basso, la risposta dal basso verso l'alto
  • L'ordine di dichiarazione nella lista MIDDLEWARE determina l'ordine di esecuzione e può causare errori sottili se non rispettato
  • Django 5.2 introduce il supporto completo ai middleware asincroni tramite gli attributi async_capable e sync_capable
  • I segnali pre_save e post_save operano a livello di singola istanza del modello, non su operazioni bulk come QuerySet.update()
  • La registrazione dei segnali nel metodo AppConfig.ready() è obbligatoria per garantire il collegamento dei receiver all'avvio dell'applicazione
  • I segnali personalizzati disaccoppiano efficacemente la logica di business seguendo il principio di inversione delle dipendenze
  • In Django 5.2, Signal.asend() e Signal.asend_robust() consentono l'invio asincrono dei segnali, evitando blocchi nelle view asincrone
  • Il middleware gestisce aspetti trasversali a livello HTTP, i segnali reagiscono a eventi interni: confondere i due ambiti rappresenta un antipattern

Inizia a praticare!

Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.

Tag

#django
#python
#middleware
#signals
#interview

Condividi

Articoli correlati