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?

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:

>> Event.objects.filter(is_active=True)
<QuerySet [<Event: Another create perhaps visit.>,....

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:

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.

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.

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:

from .managers import ActiveManager

...
active = ActiveManager()

den Manager nutzen

Wir wollen nur noch aktive Elemente erhalten:

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:

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:

'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:

objects = CategoryManager()

Wir können den Manager wie folgt nutzen:

>>> from events.models import Category
>>> result = Category.objects.all()
>>> result[1].number_of_events
6

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:

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

https://docs.djangoproject.com/en/stable/topics/db/aggregation/