Custom Managers
Der Manager stellt in Django die zentrale Schnittstelle für Datenbankabfragen im Rahmen des Object-Relational Mappers (ORM) dar. Über ihn werden QuerySets erzeugt und Operationen auf Datenbankebene ausgeführt. Jedes Model besitzt mindestens einen Manager, standardmäßig mit dem Namen objects. Dieser Default-Manager ermöglicht den Zugriff auf alle Datensätze eines Modells, etwa über Event.objects.all().
Darüber hinaus können eigene Manager definiert werden, die den Zugriff auf eine bestimmte Teilmenge der Daten kapseln. So lassen sich häufig benötigte Filterlogiken zentralisieren und wiederverwenden, anstatt sie an mehreren Stellen im Code zu wiederholen.
Das Hinzufügen zusätzlicher Manager ist die bevorzugte Methode, um Modellen tabellenbasierte Abfragen hinzuzufügen. Für Funktionen auf „Zeilenebene“, also solche, die auf eine einzelne Instanz eines Modellobjekts wirken, werden hingegen Modelmethoden verwendet und keine benutzerdefinierten Manager.
Auf diese Weise wird auch das DRY-Prinzip umgesetzt. Generell verfolgen wir in Django häufig den Ansatz fette Models, dünne Views: Geschäftslogik sollte möglichst nah am Model liegen und nicht redundant in verschiedenen Views implementiert werden. Das bedeutet jedoch nicht, dass sämtliche Logik im Model untergebracht werden sollte.
Bei umfangreicher oder komplexer Geschäftslogik ist es sinnvoll, einen separaten Service-Layer einzuführen, der übergreifende Abläufe kapselt und verhindert, dass Models überladen werden oder Verantwortlichkeiten vermischt werden.
Manager, Service-Layer und Architektur
Die Aufteilung von Geschäftslogik in Models, Manager und Service-Layer ist ein komplexes Thema der Softwarearchitektur. In diesem Buch wird darauf nur oberflächlich eingegangen. Eine vertiefte Betrachtung, auch von etwas komplexeren Managern, erfolgt in einem weiterführenden Band.
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)
Wir könnten unsere EventListView umbauen und den neuen Manager nutzen, um nur aktive Events anzuzeigen. Dazu öffnen wir die Datei event_manager/events/views.py und ersetzen die EventListView durch folgende Version:
class EventListView(ListView):
"""
Auflisten aller aktiven Events
/events
"""
model = Event
paginate_by = 10
def get_queryset(self):
return Event.active.prefetch_related("category").all()
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_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"."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"."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
>>> c = Category.objects.first()
>>> c.number_events
6
Wir könnten jetzt im Template beim Anzeigen der Kategorien auch die Anzahl der Events pro Kategorie anzeigen, ohne dass wir dafür extra die Methode number_of_events aufrufen müssen, wie wir das bisher gemacht haben.
Dazu öffnen wir event_manager/events/templates/events/category_list.html und ändern die Kategorie-Liste wie folgt ab:
{% for category in categories %}
<a href="{% url 'events:category-detail' category.pk %}">
<li class="list-group-item rounded">
{{category}}
<span class="badge rounded-pill bg-primary">{{category.number_events}}</span>
</li>
</a>
{% endfor %}
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:
>>> categories = Category.objects.annotate(n=Avg('events__min_group'))
>>> categories.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_events=Count('events'))
>>> cats.first().number_events
11
oder die Mindest-Gruppengröße eines Events innerhalb einer Kategorie:
>>> result = Category.objects.annotate(group_min=Min('events__min_group'))
>>> result[0].group_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/