Komplexe Queries
Bisher hatten wir es mit relativ einfachen Abfragen zu tun. Wir haben mit
objects.all()
alle Objekte einer Klasse selektiert und mit filter
einfache Filteraufgaben durchgeführt.
Wir wissen nun, dass Filter-Chaining bzw. das mehrfache Verwenden von
filter()
auf ein Queryset in der Generierung des AND-Operators
resultiert.
objects
ist der sogenannte Manager und all()
eine Methoden dessen.
der OR-Operator
Was ist aber zum Beispiel mit ODER
? Mit dem Pipe-Symbol können wir auch OR-Abfragen erzeugen:
>>> # Alle Events aus dem Monat Januar des Jahres 2022, die Hobbit im
>>> # Namen haben. Mehrere Filter werden als SQL "AND" betrachtet.
>>> qs = Event.objects.filter(date__year=2022).filter(date__month=1)
>>> qs = qs.filter(name__icontains="Hobbit")
>>> qs
<QuerySet [<Event: Der Hobbit-Club>]>
>>>
>>> # einfache OR-Abfrage (Name beinhaltet großes H oder Sub-Titel beinhaltet kleines u)
>>> qs = Event.objects.all()
>>> qs = qs.filter(name__contains="H") | qs.filter(sub_title__contains="u")
>>> qs
<QuerySet [<Event: Der Hobbit-Club>, <Event: Harry Potter Leserunde>, ...]>
>>> str(qs.query)
'SELECT ... FROM "events_event" WHERE ... OR ...'
Allerdings ist diese Schreibweise nicht sonderlich elegant. Wir müssen immer
qs.filter
schreiben und die jeweils davon erzeugten Querysets
mit den Pipe-Operator verbinden.
Komplexe Abfragen mit Q-Objekten
Falls wir aber komplexere Queries schreiben wollen, müssen wir das Q-Objekt nutzen.
Wir können damit Queries quasi auslagern und OR
- und AND
- Operationen elegant verschachteln.
from django.db.models import Q
>>> # Filter: alle Events mit Harry im Namen ODER aus dem Jahr 2022
>>> # UND (die im Sub-Titel ein kleines "u" beinhalten oder aus dem Monat Februar sind)
>>> qs = Event.objects.filter(Q(name__contains="Harry") | Q(date__year=2022))
>>> qs = qs.filter(Q(sub_title__contains="u") | Q(date__month=2))
>>> qs
<QuerySet [<Event: Outdoor Boxen>]>
Ein weiteres Beispiel mit einer OR
Verknüpfung mit dem |
- Operator (Pipe).
Events die mit Sp anfangen UND (aus der Kategorie Sport oder aus der Kategorie Kochen sind):
qs = Event.objects.filter(
Q(name__startswith='Sp'),
(Q(category='Sport') | Q(category='Kochen'))
)
Ein Beispiel mit der Negation eines Q - Objekts mit dem ~
- Operator.
from django.db.models import Q
recent_years_end_events_excluding_books = (
Event.objects.filter(
Q(date__year__gt=2021),
~Q(category__name="Bücherwurm"),
(Q(date__month=11) | Q(date__month=12)),
)
)
Wir selektierten im Beispiel alle Events nach dem Jahr 2021, die NICHT aus der Kategorie Bücherwurm sind und entweder aus dem Monat November und Dezember stammen.
Um den Code noch sauberer zu halten, könnte man die Q-Objekte
auch auslagern und später im Code
verwenden. Das könnte mal bei wiederkehrenden Abfragen durchaus Sinn
ergeben, sollte aber auch nicht übertrieben werden..
from django.db.models import Q
greater_2021 = Q(date__year__gt=2021)
not_books = ~Q(category__name="Bücherwurm")
month_11 = Q(date__month=11)
month_12 = Q(date__month=12)
recent_years_end_events_excluding_sport = (
Event.objects.filter(
greater_2021,
not_books,
(month_11 | month_12),
)
)
Mehr zu Q-Objekten findet sich in der Django-Doku: https://docs.djangoproject.com/en/ref/topics/db/queries/#complex-lookups-with-q
Exkludieren (Sql NOT)
Um das Verhalten des NOT
- Operators abzubilden, können wir Objekte exkludieren.
Die Methode exlude()
liefert uns dabei wieder ein Queryset.
1# Alle Events, die nicht dem Kriterium entsprechen
2qs = Event.objects.exclude(name="Kafka Freunde")
3qs = qs.filter(year__2022)
4<QuerySet [<Event: Harry Potter Leserunde>, <Event: Hobbit Club>, ...]>
F-Expressions
Eine F()-Expression
repräsentiert den Wert eines Modelfelds, den
transformierten Wert eines Modelfelds oder eine annotierte Spalte. Es
ermöglicht, auf Modelfeldwerte zu verweisen und Datenbankoperationen mit ihnen
durchzuführen, ohne sie tatsächlich aus der Datenbank in den Arbeitsspeicher
ziehen zu müssen.
Nehmen wir an, wir möchten das min_group
-Attribut jedes Event-Objektes um
1 hochzählen. Der erste, falsche Weg wäre, alle Event-Objekte mit all()
zu
laden, über sie zu iterieren und den Wert um 1 hochzählen.
Der bessere Weg ist über eine F-Expression
, mit dem man direkt auf das Modelfeld verweisen kann.
1. Beispiel: Min_group Feld jedes Events um 1 erhöhen:
# falsch
events = Event.objects.all()
for event in events:
event.min_group += 1
event.save()
# richtig
from django.db.models import F
Event.objects.update(min_group=F('min_group') + 1)
Wir greifen mit F('min_group')
auf den Feldwert zu und führen gleichzeitig ein update
mit dem neuen Feldwert aus.
2. Beispiel: Name-Feld aller Einträge von Unterstrichen befreien und durch
Leerzeichen ersetzen. Dazu benötigen wir die Funktionen Replace
und
Value
aus den Model-Funktionen.
Value (aus der Django-Doku):
Wenn man den Wert einer ganzen Zahl, eines booleschen Wertes oder einer
Zeichenkette in einem Ausdruck darstellen muss, kann man diesen Wert in einen
Value()
einschließen. Ohne Value()
würde das Statement in einem Fehler
resultieren.
from django.db.models.functions import Replace
from django.db.models import Value
Event.objects.update(name=Replace('name', Value("_"), Value(" ")))
Aggregate
Generiere zusammenfassende Werte über ein gesamtes QuerySet
.
Aggregate geben meistens Werte wie Zahlen zurück. zb. Max
oder Min
eines Feldes.
1. Beispiel: Wir benötigen die Kategorie mit dem ältesten Event. Dazu
importieren wir uns einige der Aggregatsfunktionen und wählen die Funktion
Min
:
>>> from django.db.models import Sum, Max, Min, Count, Avg
>>>
>>> oldest_event = Event.objects.aggregate(Min('date'))
{'date__min': datetime.datetime(2022, 5, 13, 11, 14, 36, tzinfo=<UTC>)}
# prüfen mit order_by
>>> Event.objects.order_by("date").first().date
datetime.datetime(2022, 5, 13, 11, 14, 36, tzinfo=<UTC>)
In der Ausgabe sehen wir, dass das Ergebnis von aggregate
ein Dict mit dem
Feldnamen als Key und dem Feldwert als Value. Wenn wir bei dem Aufruf von
aggregate keinen Feldnamen angeben, ist er Name hier date__min
.
Wir könnten aber auch einen Feldnamen definieren:
>>> oldest_event = Event.objects.aggregate(oldest=Min('date'))
>>> oldest_event["oldest"]
datetime.datetime(2022, 5, 13, 11, 14, 36, tzinfo=<UTC>)
2. Beispiel: Mit der Avg - Funktion lässt sich zum Beispiel auch die durchschnittliche Gruppengröße pro Kategorie herausfinden. Wir werden das Feld min_group allerdings erst später im Buch zum Event-Model hinzufügen, deshalb resultiert diese Abfrage aktuell noch in einem Fehler:
>>> categories = Category.objects.aggregate(avg_gz=Avg('events__min_group'))
>>> categories["avg_gz"]
Wir benutzen hier den doppelten Unterstrich, um Felder über Beziehungen hinweg
zu referenzieren. Hier referenzieren wir auf das Feld min_group
des
Event-Models.
Annotationen
Mit Annotationen lassen sich zusätzliche Felder in die Ausgabemenge einfügen. Es handelt sich hier also um Aggregate für jedes Element im Set.
Beispiel 1: Ein neues Feld mit fixem Wert hinzufügen:
>> b = Event.objects.annotate(fixed_value=Value(3))
>> b.first().fixed_value
3
Beispiel 2: Alle aktiven Events zählen. Um ein GROUP BY
zu erstellen,
verwenden wir eine Kombination aus values
und annotate
:
number_of_active_events = (Event.objects
.filter(is_active=True)
.values("is_active")
.annotate(
total=Count("id")
)
)
number_of_active_events[0]["total"]
4
Die Methode values()
erwartet optional als Argumente, die Feldnamen, die
selektiert werden sollen, in unserem Fall also nur die Spalte is_active
.
Mehr zur Methode values in der Django Doku: https://docs.djangoproject.com/en/stable/ref/models/querysets/#values
Beispiel 3: Die Anzahl der Events zählen, die einer Kategorie zugeordnet
sind. Wir fügen jeder Kategorie ein neues Feld num_of_events
hinzu, welches
man später per Dot-Notation addressieren kann.
categories = (Category.objects
.annotate(
num_of_events=Count("events"))
)
categories.first().num_of_events
7
Das neue Feld kann auch für Filter-Operationen genutzt werden.
Beispiel 4: Im folgenden Beispiel zählen wir mit Count
die Events, die das Zeichen
„c“ im Namen haben und filtern dann die Kategorien, die mehr als zwei solcher
Events besitzen. Count
erwartet als Keyword-Argument optional filter
, an
das wir ein Q-Objekt übergeben.
categories_with_c = (Category.objects
.annotate(
c=Count('events',
filter=Q(events__name__contains='c')
))
.filter(
c__gt=2
)
)
categories_with_c
<QuerySet [<Category: Freizeit>, <Category: Sport>]>
Beispiel 5: Events, die mindestens 40 Tage entfernt sind:
from django.db.models.functions import Now
from datetime import timedelta
far_away = (Event.objects
.annotate(
dg=F('date') - Now())
.filter(
dg__gt=timedelta(days=40)
)
)
Wir annotieren jede Zeile mit der Differenz von Eventdatum und aktuellem
Zeitpunkt und filtern auf Basis dieses Ergebnisses alle Events, bei denen diese
Differenz größer als 40 Tage ist. Dazu nutzen wird die timedelta-Methode
des
datetime-Moduls.