Django 5.2: Custom Middleware und Signal-Handling für technische Interviews

Django 5.2 Middleware und Signals meistern: Middleware-Pipeline, asynchrone Middleware, pre_save/post_save Signals und häufige Interview-Fragen praxisnah erklärt.

Django 5.2 Custom Middleware und Signal-Handling Tutorial

Django 5.2 führt als LTS-Release zahlreiche Verbesserungen ein, die sowohl die Middleware-Architektur als auch das Signal-System betreffen. Beide Mechanismen gehören zum Standardrepertoire technischer Interviews für Django-Entwickler und erfordern ein tiefes Verständnis der internen Request-Verarbeitung sowie der ereignisgesteuerten Kommunikation zwischen Applikationskomponenten. Dieser Artikel behandelt die Middleware-Pipeline, asynchrone Middleware in Django 5.2, das Signal-System mit seinen gängigen Patterns und die häufigsten Stolperfallen, die in Vorstellungsgesprächen abgefragt werden.

Interview-Kurzreferenz

Die vier Kernthemen für Django-Interviews zu Middleware und Signals: (1) Die Middleware-Pipeline als Zwiebelschalen-Architektur, (2) die korrekte Reihenfolge in MIDDLEWARE, (3) post_save vs. pre_save und deren Einsatzgebiete, (4) asynchrone Middleware und Signals mit async_capable bzw. asend in Django 5.2.

Wie die Django-Middleware funktioniert

Die Middleware in Django folgt einem Zwiebelschalen-Modell (Onion Architecture). Jede eingehende HTTP-Anfrage durchläuft die konfigurierten Middleware-Klassen von oben nach unten, bevor sie die View-Funktion erreicht. Die Antwort durchläuft dieselben Middleware-Klassen in umgekehrter Reihenfolge. Jede Middleware-Klasse erhält im Konstruktor die Referenz auf get_response, die entweder die nächste Middleware in der Kette oder die eigentliche View-Funktion darstellt.

Dieses Pattern wird in Interviews häufig abgefragt, da es das Verständnis des Request-Response-Lifecycle in Django prüft. Die __init__-Methode wird einmalig beim Serverstart aufgerufen und eignet sich für initiale Konfiguration, während __call__ bei jedem Request ausgeführt wird.

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

Aufbau einer Request-Logging-Middleware

Ein praxisnahes Beispiel für technische Interviews ist eine Logging-Middleware, die Methode, Pfad, Statuscode, Ausführungsdauer und IP-Adresse jedes Requests protokolliert. Diese Middleware demonstriert mehrere Konzepte gleichzeitig: die Zeitmessung über time.monotonic(), das Anheften von Metadaten an das Request-Objekt und die optionale Fehlerbehandlung über process_exception.

Die Methode process_exception wird ausschließlich dann aufgerufen, wenn die View eine unbehandelte Exception wirft. Sie erhält das Request-Objekt und die Exception als Parameter. Gibt sie None zurück, übernimmt Djangos Standard-Fehlerbehandlung. Gibt sie ein HttpResponse-Objekt zurück, wird dieses als Antwort verwendet.

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

Middleware-Reihenfolge

Die Reihenfolge der Middleware-Klassen in der MIDDLEWARE-Liste von settings.py ist entscheidend und wird in Interviews regelmäßig abgefragt. SecurityMiddleware steht an erster Stelle, da sie sicherheitsrelevante Header setzt, bevor andere Middleware den Request verarbeitet. SessionMiddleware muss vor AuthenticationMiddleware stehen, da die Authentifizierung auf Session-Daten angewiesen ist. CsrfViewMiddleware muss vor jeder Middleware stehen, die auf POST-Daten zugreift.

Eigene Middleware wird typischerweise nach dem Django-Kernstack platziert, damit alle eingebauten Sicherheits- und Authentifizierungsmechanismen bereits greifen.

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

Asynchrone Middleware in Django 5.2

Django 5.2 bietet vollständige Unterstützung für asynchrone Middleware. Durch das Setzen von async_capable = True und sync_capable = False wird die Middleware als rein asynchron deklariert. In diesem Modus ist get_response ein Coroutine, das mit await aufgerufen wird. Diese Architektur eignet sich besonders für I/O-gebundene Operationen wie externe API-Aufrufe oder Datenbankabfragen innerhalb der Middleware.

In Interviews wird häufig nach dem Unterschied zwischen sync_capable und async_capable gefragt. Eine Middleware kann beide Flags auf True setzen, um sowohl in synchronen als auch in asynchronen Kontexten zu funktionieren. Django wählt dann automatisch den passenden Ausführungsmodus basierend auf dem Servertyp (WSGI vs. ASGI).

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

Bereit für deine Django-Interviews?

Übe mit unseren interaktiven Simulatoren, Flashcards und technischen Tests.

Django Signals: Das Publish-Subscribe-Pattern

Das Signal-System in Django implementiert ein Publish-Subscribe-Pattern, das eine lose Kopplung zwischen Applikationskomponenten ermöglicht. Ein Sender (Publisher) löst ein Signal aus, ohne zu wissen, welche Empfänger (Subscriber) darauf reagieren. Dieses Pattern ist besonders nützlich, wenn mehrere unabhängige Aktionen als Reaktion auf ein einzelnes Ereignis ausgeführt werden sollen, etwa das Erstellen eines Benutzerprofils nach der Registrierung oder das Senden einer Benachrichtigung nach einer Bestellung.

Django liefert eine Reihe eingebauter Signals wie pre_save, post_save, pre_delete, post_delete, request_started und request_finished. Darüber hinaus können eigene Signals für domänenspezifische Ereignisse definiert werden.

post_save für die Profilerstellung

Das post_save-Signal wird nach dem erfolgreichen Speichern einer Modellinstanz ausgelöst. Der Parameter created gibt an, ob es sich um eine Neuanlage (True) oder eine Aktualisierung (False) handelt. Dieses Unterscheidungsmerkmal ist in Interviews besonders relevant, da viele Entwickler den created-Parameter nicht korrekt nutzen und dadurch ungewollte Seiteneffekte bei Updates auslösen.

Das folgende Beispiel zeigt das automatische Erstellen eines Benutzerprofils nach der Registrierung. Der @receiver-Dekorator verbindet die Funktion mit dem Signal und dem spezifischen Sender.

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

pre_save für Validierung und Transformation

Das pre_save-Signal wird vor dem Speichern ausgelöst und eignet sich für Datentransformationen wie die automatische Slug-Generierung. Da pre_save vor der Datenbankoperation feuert, können Felder der Instanz noch modifiziert werden, ohne dass ein zusätzlicher save()-Aufruf nötig ist.

Das Beispiel zeigt eine Slug-Generierung mit Eindeutigkeitsprüfung. Die Schleife stellt sicher, dass keine Kollisionen mit bestehenden Slugs auftreten, indem bei Bedarf ein Zähler angehängt wird. Der exclude(pk=instance.pk)-Aufruf verhindert, dass ein bereits gespeicherter Artikel mit sich selbst kollidiert.

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

Registrierung von Signals in AppConfig.ready()

Signals müssen importiert werden, damit Django die Receiver-Funktionen registriert. Der empfohlene Ort dafür ist die ready()-Methode der AppConfig-Klasse. Diese Methode wird einmalig beim Start der Applikation aufgerufen, nachdem alle Apps geladen sind. Das Importieren des Signals-Moduls in ready() stellt sicher, dass die @receiver-Dekoratoren ausgeführt und die Handler registriert werden.

Ein häufiger Interview-Fehler ist das Importieren von Signals auf Modulebene in models.py oder views.py, was zu zirkulären Imports führen kann. Die ready()-Methode vermeidet dieses Problem zuverlässig.

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

Benutzerdefinierte Signals für Domänenereignisse

Neben den eingebauten Signals können eigene Signals für domänenspezifische Ereignisse definiert werden. Dies ermöglicht eine saubere Trennung zwischen Geschäftslogik und Seiteneffekten. Im folgenden Beispiel löst der Bestellservice nach Abschluss einer Bestellung ein order_completed-Signal aus, auf das ein Benachrichtigungsmodul reagiert, ohne dass die beiden Module direkt voneinander abhängen.

Diese Architektur wird in Interviews als Beispiel für das Open-Closed-Prinzip herangezogen: Neue Reaktionen auf eine Bestellung (z.B. Analytics, Lagerbestandsaktualisierung) können durch zusätzliche Receiver ergänzt werden, ohne den Bestellservice selbst zu verändern.

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

Häufige Interview-Stolperfallen

QuerySet.update() löst keine Signals aus

QuerySet.update() und QuerySet.delete() umgehen das Signal-System vollständig. Nur instance.save() und instance.delete() lösen pre_save/post_save bzw. pre_delete/post_delete aus. Diese Tatsache ist eine der am häufigsten gestellten Interview-Fragen zu Django Signals.

  • Zirkuläre Imports: Signals niemals auf Modulebene in models.py importieren. Stattdessen die ready()-Methode der AppConfig verwenden, um das Signals-Modul erst nach dem vollständigen Laden aller Apps zu importieren.
  • Endlosschleifen: Ein post_save-Receiver, der instance.save() aufruft, löst das gleiche Signal erneut aus. Stattdessen Model.objects.filter(pk=instance.pk).update(...) verwenden oder ein Guard-Flag wie if not getattr(instance, '_skip_signal', False) einbauen.
  • Fehlender created-Check: Ohne die Prüfung des created-Parameters in post_save wird die Logik bei jedem Update ausgeführt, nicht nur bei der Neuanlage.
  • Keine Transaktionssicherheit: Signals werden nicht automatisch in die Datenbanktransaktion der auslösenden Operation eingebunden. Für transaktionssichere Ausführung transaction.on_commit() verwenden.
  • Performance bei Bulk-Operationen: bulk_create() und bulk_update() lösen standardmäßig keine Signals aus. Erst ab bestimmten Django-Versionen und mit expliziter Konfiguration werden Signals bei Bulk-Operationen unterstützt.
Django 5.2 LTS: Langzeitunterstützung

Django 5.2 ist ein Long-Term-Support-Release mit garantierten Sicherheitsupdates bis April 2028. Es bietet vollständige asynchrone Unterstützung sowohl für Middleware (async_capable) als auch für Signals (Signal.asend() und Signal.asend_robust()). Diese Async-Features werden in Interviews zunehmend relevant, da immer mehr Django-Projekte auf ASGI-Server wie Daphne oder Uvicorn migrieren.

Middleware vs. Signals: Vergleichstabelle

| Aspekt | Middleware | Signals | |---|---|---| | Request-/Response-Modifikation | Ja | Nein | | Authentifizierung und Autorisierung | Ja | Nein | | Modell-Lifecycle-Hooks | Nein | Ja | | Querschnittsbelange (Logging, Timing) | Ja | Möglich, aber nicht ideal | | Entkoppelte Geschäftslogik | Nein | Ja | | Async-Unterstützung in Django 5.2 | Vollständig | Vollständig (asend/asend_robust) | | Ausführungsbereich | Jeder HTTP-Request | Spezifische Events (save, delete, custom) |

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Fazit

  • Middleware folgt dem Zwiebelschalen-Modell: Jeder Request durchläuft die Middleware-Klassen sequenziell, die Antwort in umgekehrter Reihenfolge
  • Die Reihenfolge in MIDDLEWARE ist entscheidend: SecurityMiddleware zuerst, eigene Middleware nach dem Django-Kernstack
  • Django 5.2 unterstützt vollständig asynchrone Middleware mit async_capable = True für I/O-gebundene Operationen
  • post_save erfordert stets die Prüfung des created-Parameters, um ungewollte Ausführung bei Updates zu vermeiden
  • pre_save eignet sich für Datentransformationen, da Feldänderungen ohne zusätzlichen save()-Aufruf wirksam werden
  • Signals müssen über AppConfig.ready() registriert werden, um zirkuläre Imports zu vermeiden
  • QuerySet.update() und bulk_create() umgehen das Signal-System und erfordern alternative Strategien
  • Benutzerdefinierte Signals implementieren das Open-Closed-Prinzip und ermöglichen entkoppelte Geschäftslogik

Fang an zu üben!

Teste dein Wissen mit unseren Interview-Simulatoren und technischen Tests.

Tags

#django
#python
#middleware
#signals
#interview

Teilen

Verwandte Artikel