# 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. - Published: 2026-04-29 - Updated: 2026-04-29 - Author: SharpSkill - Tags: django, python, middleware, signals, interview - Reading time: 9 min --- 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 ``` ## 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) | ## 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 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/de/blog/django/django-5-2-custom-middleware-signals