Django Signals: wann und wie richtig einsetzen
Inhalt
Django Signals ermöglichen es, lose gekoppelte Komponenten über Ereignisse zu verbinden. Klingt gut — und ist es auch, wenn man sie richtig einsetzt. In meiner Praxis sehe ich aber oft Projekte, in denen Signals zu einem Spaghetti-Geflecht aus versteckten Abhängigkeiten geführt haben.
Was sind Signals?
Das Observer-Pattern in Django: Ein Sender schickt ein Signal, beliebig viele Receiver reagieren darauf. Sender und Receiver kennen sich nicht direkt.
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model
User = get_user_model()
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)Wichtig: Signals-Handler müssen importiert werden, damit sie registriert sind. Am saubersten in AppConfig.ready():
# myapp/apps.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = "myapp"
def ready(self):
import myapp.signals # Registrierung auslösenEingebaute Signals
Die wichtigsten eingebauten Signals:
pre_save/post_save— vor/nach Model.save()pre_delete/post_delete— vor/nach Model.delete()m2m_changed— ManyToMany-Feld wird geändertrequest_started/request_finished— HTTP-Anfrage Lebenszykluspost_migrate— nach jeder Migration (ideal für Initialdaten)
# post_save mit created-Flag
@receiver(post_save, sender=Order)
def handle_order_saved(sender, instance, created, **kwargs):
if created:
send_confirmation_email.delay(instance.id)
else:
if instance.status == "shipped":
send_shipping_notification.delay(instance.id)Eigene Signals erstellen
# myapp/signals.py
from django.dispatch import Signal
# Signal definieren
order_completed = Signal() # Kann kwargs mitgeben
# Sender
order_completed.send(sender=Order, order=order_instance, user=request.user)
# Receiver
@receiver(order_completed)
def on_order_completed(sender, order, user, **kwargs):
update_user_stats(user)
generate_invoice.delay(order.id)Best Practices
- Immer
**kwargsakzeptieren — Signals können in Zukunft neue Parameter bekommen - Keine Fehler in Receivern verschlucken — Exceptions in Receivern brechen den gesamten Signal-Dispatch ab
- Receivers schlank halten — aufwändige Logik in Celery-Tasks auslagern
dispatch_uidsetzen — verhindert doppelte Registrierung beim Reload
post_save.connect(
create_user_profile,
sender=User,
dispatch_uid="create_user_profile_on_save" # Eindeutige ID
)Wann Signals vermeiden
Signals sind kein Ersatz für direkten Code. Wenn Sender und Receiver im gleichen Modul leben, ist ein direkter Methodenaufruf fast immer besser.
Vermeide Signals wenn:
- Sender und Receiver im gleichen App-Kontext liegen (direkte Kopplung ist klarer)
- Die Ausführungsreihenfolge wichtig ist (mehrere Receiver sind nicht geordnet)
- Der Signal-Handler Daten zurück an den Sender muss
- Tests dadurch schwer schreibbar werden (Signal-Handler lassen sich mocken, aber es ist Aufwand)
