.. _custom_managers: .. index:: single: Model Manager single: Annotations single: objects single: DRY Custom Managers ************************ Der Manager ist eine Schnittstelle, mit der Datenbank-Operationen für ein Model ermöglicht werden. Jedes Model in Django besitzt mindestens einen Manager. Der Default-Manager eines Objektes heisst ``objects``. Der Default-Manager bezieht sich immer auf den ganzen Datensatz. Wenn wir ``Event.objects.all()`` ausführen, erhalten wir also alle Datensätze von Event. Wir können aber auch eigene Manager entwickeln, der nur eine Sub-Menge dieser Daten anbietet. Wozu könnte sowas sinnvoll sein? .. admonition:: Custom Managers Das Hinzufügen zusätzlicher Manager ist die bevorzugte Methode, um Modellen tabellenbasierte Abfragen hinzuzufügen. (Für Funktionen auf „Zeilenebene“ – d. h. Funktionen, die auf eine einzelne Instanz eines Modellobjekts wirken – verwenden wir Modelmethoden, keine benutzerdefinierten Manager) Wir setzen damit auch das ``DRY-Prinzip`` um. Generell sind wir eher an "fetten" Models und "dünnen" Views interessiert. Geschäftslogik sollte am Model integriert werden und nicht an vielen Stellen in den Views. aktive Events ---------------- In unserem Model haben wir einen Flag, den wir bisher noch gar nicht genutzt haben, das Attribut ``is_active``, ein Boolean-Feld, dass angeben soll, ob der Event überhaupt angezeigt werden soll oder nicht. Deaktivierte Events könnten zum Beispiel Stornierungen, Verstöße gegen die Hausregeln oder einfach nur veraltete Events sein. Wenn wir bisher die Menge aller aktiven Events erhalten wollen, mussten wir wie folgt vorgehen: .. code-block:: python >> Event.objects.filter(is_active=True) ,.... Da wir generell auf unserer Seite nur aktive Events betrachten wollen, müssten wir uns also überall wiederholen. Das würde gegen das ``DRY-Prinzip`` verstoßen. Besser ist es also, so eine Aktion zentral zu definieren. ein Active-Manager --------------------- Wir könnten aber auch einen eigenen Manager dafür schreiben! Dazu öffnen wir die Datei ``event_manager/events/models.py`` und ändern das Event-Model wie folgt ab: .. code-block:: python class Event(DateMixin): class Meta: ordering = ["name"] verbose_name = _("event") verbose_name_plural = _("events") [..] # Diese beiden Zeilen hinzufügen objects = models.Manager() active = ActiveManager() Der erste Manager, den wir dem Model hinzufügen, ist der Default-Manager, unabhängig von seinem Namen. Um also den Default-Manager ``objects`` nicht zu verlieren, geben wir ``objects = models.Manager()`` nochmal explizit hier an. Nun wollen wir einen eigenen Manager ``Active-Manager`` entwickeln. Wir fügen folgende Klasse zu ``event_manager/events/models.py`` hinzu. Die Klasse muss oberhalb der Klasse ``Event`` definiert werden. .. code-block:: python class ActiveManager(models.Manager): """Manager mit einem Queryset, das nur aktive Elemente zurückgibt.""" def get_queryset(self): return super().get_queryset().filter(is_active=True) Die Klasse ``ActiveManager`` muss von ``models.Manager`` erben, um später auch als Manager nutzbar zu sein. Dann überschreiben wir die Methode ``get_queryset()``, die uns das Queryset des Managers liefert. In unserem Fall also die nach dem Aktiv-Status gefilterten Objekte. Wie wir sehen können, ist der Manager vom Model, wo er eingesetzt wird, erstmal unabhängig. Er überschreibt lediglich die Methode ``get_queryset``. .. admonition:: Auslagern in eigene Dateien Wenn man mehrere Manager-Klassen definiert hat, lohnt sich das Auslagern in eine Datei ``event_manager/events/managers.py``. Danach wird dieser Manager in die ``models.py`` importiert: .. code-block:: python from .managers import ActiveManager ... active = ActiveManager() den Manager nutzen --------------------- Wir wollen nur noch aktive Elemente erhalten: .. code-block:: python active_events = Event.active.all() print(active_events) ein eigener Category-Manager ------------------------------ Wir wollen noch einen Manager entwicklen, der uns alle Kategorien plus die Anzahl an Events jeder Kategorie zurückgibt. Dazu nutzen wir ``annotations``. D.h. wir fügen jeder Ergebniszeile eine SQL-Abfrage ein Feld hinzu, welches ein Aggregat einer anderen Tabelle darstellt. Im konkreten Falls sind wir an der Anzahl der Events pro Kategorie interessiert: .. code-block:: python class CategoryManager(models.Manager): def get_queryset(self): return super().get_queryset().annotate( number_of_events=Count('events') ) Wir fügen jeder Zeile des Ergebnisses ein neues Feld ``number_of_events`` hinzu. Das SQL dazu sieht in etwa so aus: .. code-block:: bash 'SELECT "events_category"."id", "events_category"."created_at", "events_category"."updated_at", "events_category"."name", "events_category"."sub_title", "events_category"."slug", "events_category"."description", COUNT("events_event"."id") AS "number_of_events" FROM "events_category" LEFT OUTER JOIN "events_event" ON ("events_category"."id" = "events_event"."category_id") GROUP BY "events_category"."id", "events_category"."created_at", "events_category"."updated_at", "events_category"."name", "events_category"."sub_title", "events_category"."slug", "events_category"."description"' und registrieren den Manager in der Klasse ``Category`` als Default-Manager: .. code-block:: python objects = CategoryManager() Wir können den Manager wie folgt nutzen: .. code-block:: python >>> from events.models import Category >>> result = Category.objects.all() >>> result[1].number_of_events 6 .. admonition:: Annotations VS. Aggregate Annotations und Aggregate sind ein gutes Mittel, um die Ausführungsgeschwindigkeit einer website zu optimieren, da die Operationen direkt auf der Datenbank ausgeführt werden, und nicht später im langsamen Python Code. ``Aggregate`` sind zusammenfassende Werte über ein komplettes Queryset. Zum Beispiel die Anzahl aller Events der Kategorien Sport und Bücherwurm. >>> from django.db.models import Q, Avg, Sum, Min, Max >>> Category.objects.filter(Q(name="Talk") | Q(name="Sport"))\ >>> .aggregate(anzahl_events=Count('events')) {anzahl_events: 87} oder die durchschnittliche min_group aller Events: >>> Category.objects.all().aggregate(n=Avg('events__min_group')) {'n': 8.8059701stable53731} ``Annotationen`` hingegen sind Aggregate auf Objekt-Ebene. Das heisst, jedem Eintrag im Queryset wird ein Feld hinzugefügt, zb. jeder Kategorie die durchschnittliche ``min_group`` ihrer Events: >>> cats = Category.objects.annotate(n=Avg('events__min_group')) >>> cats.first().n 8.095238095238095 oder eine Abfrage, wie viele Events pro Kategorie im Namen die Zeichenkette "in" haben. >>> b = Category.objects.annotate(c=Count('events', filter=Q(events__name__contains='in'))) >>> b.first().c 11 Oder eben die Anzahl der Events, die einer Kategorie zugeordnet sind: >>> cats = Category.objects.annotate(number_of_events=Count('events')) >>> cats.first().number_of_events 11 oder die Mindest-Gruppengröße eines Events innerhalb einer Kategorie: >>> result = Category.objects.annotate(cat_min=Min('events__min_group')) >>> result[0].cat_min 0 Um einen String an ein Varchar-Feld anzuhängen, gibt es die Funktion Concat. Hier im Beispiel hängen wir an jeden Eintrag des sub_title den String -> deprecated an: .. code-block:: python from django.db.models.functions import Concat from django.db.models import Value Category.objects.update( sub_title=Concat("sub_title", Value("-> deprecated")) ) Mehr zum Thema Annotationen und Aggregate ------------------------------------------ ``_