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/