Queryset: Datenbankeinträge selektieren
Ein QuerySet
repräsentiert eine Sammlung von Objekten aus der Datenbank. Mit Filter können die Abfrageergebnisse eingegrenzt werden. In SQL-Begriffen entspricht ein QuerySet
einer SELECT-Anweisung und ein Filter ist eine einschränkende Klausel wie WHERE
oder LIMIT
.
Ein QuerySet kann erstellt, gefiltert, aufgeteilt und im allgemeinen
weitergegeben werden, ohne die Datenbank tatsächlich anzufragen. Tatsächlich
arbeitet ein Queryset lazy
, d.h. es wird erst ausgeführt, wenn es
tatsächlich auch konsumiert wird. Zum Beispiel dem Auflisten und Filtern aller
Events, die mit dem Buchstaben M beginnen.
Was ist ein Queryset eigentlich?
Ein Queryset ist ein listenartiger, sequentieller Datentyp, der aber nicht mit
einer Liste verwechselt werden darf.
Die Klasse Queryset hat selbst viele Methoden, deren Ergebnis wiederum ein
Queryset ist, zum Beispiel die Methode filter
.
Wie erstellt man ein Queryset?
Im vorherigen Kapitel die Model-Api
haben wir gesehen, dass zum Beispiel die Methode all()
des Managers ein Queryset liefert. Das Ergebnis dieser Methode
ist ein Objekt der Klasse Queryset
mit allen Objekten der Models.
Andere Methode der Klasse hingegen erzeugen kein Queryset, zum Beispiel die Methode count()
, die wir auch schon beim Manager gesehen hatten.
Tatsächlich lassen sich die meisten Methoden des Manager auch auf ein Queryset anwenden. Zum Beispiel die Methode get()
. Nur all()
, create()
und ein paar andere sind dem Manager vorbehalten.
Queryset-Methoden, die ein Queryset zurückliefern https://docs.djangoproject.com/en/stable/ref/models/querysets/#methods-that-return-new-querysets
Queryset-Methoden, die kein Queryset zurückliefern https://docs.djangoproject.com/en/stable/ref/models/querysets/#methods-that-do-not-return-querysets
Bevor wir uns mit den Querysets beschäftigen, legen wir vorab aber noch zwei
Events in der Kategorie Bücherwurm
an und ein Objekt in der Kategorie
Sport.
>>> d = timezone.now() + timedelta(days=20)
>>> Event.objects.create(name="Der Hobbit-Club", category=books, date=d")
<Event: Der Hobbit-Club>
>>>
>>> slam_date = timezone.now() + timedelta(days=60)
>>> Event.objects.create(name="Poetry Slam", category=books, date=d")
<Event: Poetry Slam>
>>>
>>> d = timezone.now() + timedelta(days=2)
>>> Event.objects.create(name="Outdoor Boxen", category=sport, date=d)
>>>
>>> # die Methode all() des Managers liefert uns ein Queryset mit allen
>>> # Event-Objekten in der Datenbank
>>> qs = Event.objects.all()
>>> qs
<QuerySet [<Event: Hitchhikers Club>, <Event: Der Hobbit-Club>, ...]>
>>>
>>> type(qs)
<class 'django.db.models.query.QuerySet'>
>>>
>>> # Wir können uns auch das SQL angucken, das produziert wurde:
>>> str(qs.query)
'SELECT "events_event"."id", "events_event"."created_at", ...'
>>>
>>> # um auf ein Objekt der Ergebnismenge zuzugreifen, nutzen wir zb. den
>>> # den Index-Operator, oder first(), oder last()
>>> qs[0]
<Event: Der Hobbit-Club>
>>>
>>> qs.first()
<Event: Der Hobbit-Club>
>>>
>>> qs.last()
<Event: Outdoor Boxen>
Slicing von Querysets
Wenn wir nur eine Teilmenge des Querysets benötigen, können wir das Queryset auch Slicen, wie man es von Python-Listen gewohnt ist.
Hier sind zwei Dinge zu beachten: Einerseits kann auf einem per Slicing
erstellen Queryset kein Filter mehr angewandt werden und andererseits wird
aus der Slicing Operation tatsächlich eine SQL Abfrage mit der LIMIT
Anweisung
erstellt. Diese Operation ist also performant, obwohl es auf den
ersten Blick wie banales List-Slicing aussieht. Ein Queryset ist aber keine
Liste, sondern eine weit mächtigere Datenstruktur.
1>>> result = Event.objects.all()[:2]
2>>> result
3<QuerySet [<Event: Der Hobbit-Club>, <Event: Harry Potter Leserunde>]>
4>>>
5>>> str(result.query)
6'SELECT ... FROM "events_event" ... ASC LIMIT 2'
7
8>>> result.filter(name__contains="Hobbit")
9AssertionError: Cannot filter a query once a slice has been taken.
Der Python-Anfänger stellt sich an dieser Stelle vielleicht die Frage, wie das
in Python überhaupt geht, das also aus einer Operation, die aussieht wie
Slicing, eine Datenbankanfrage generiert wird. Das Stichwort hier ist
Operator Overloading
, welches in Python gerne und häufig eingesetzt wird.
Field Lookups - Querysets filtern
Django bietet über die Methode filter
unzählige und den Rahmen dieses
Tutorials sprengende Möglichkeiten an, Querysets zu filtern. Hauptbestandteil
dieser Filter sind sogenannte Field Lookups
von denen wir hier einige
exemplarisch vorstellen.
Das Schema der Field-Lookups ist immer identisch: <FELDNAME>__<FIELD LOOKUP>
Unter der Haube sind diese Field Lookups nichts weiter als Python-Funktionen, die den Lookup dann in eine SQL-Entsprechung umformen.
exact / iexact
der exakte Match. Wenn der Vergleichswert None ist, wir das als SQL Null interpretiert. exact liefert als Rückgabewert ausnahmslos ein Queryset, auch wenn kein Objekt der Bedingung entspricht.
>>> # Event mit der ID 41
>>> events = Event.objects.all().filter(id__exact=41)
>>> events
<QuerySet [<Event: Harry Potter Leserunde>]>
>>>
>>> # alle Events ohne Untertitel. filter() kann auch direkt auf
>>> # den Manager angewandt werden, auf all() kann also verzichtet werden
>>> events = Event.objects.filter(sub_title__exact=None)
>>> events
<QuerySet [<Event: Der Hobbit-Club>, <Event: Harry Potter Leserunde>,..]>
>>>
>>> # alle Events mit Namen Kafka Freunde (caseinsensitive)
>>> events = Event.objects.all().filter(name__iexact='kafka freunde')
events
<QuerySet [<Event: Kafka Freunde>]>
Es besteht bei exact
übrigens absolut kein Unterschied zu dem Benutzen des
Istgleich-Zeichens. Deshalb ist der Nutzen dieses Lookups tatsächlich auch
nicht sonderlich ersichtlich.
>>> # hier die Variante mit Istgleich und ohne exact.
>>> events = Event.objects.filter(id=41)
contains / icontains
Prüft nach, ob sich ein Element in einer Sequenz befindet. Hinweis für Sqlite: Da SQLite keine Groß- und Kleinschreibung unterstützt, arbeiten die Methoden mit dem i-Präfix genau wie ohne.
>>> events = Event.objects.all()
>>>
>>> # alle Events, die das Wort "lesen" in der Beschreibung haben
>>> qs = events.filter(description__contains='lesen')
>>> qs
<QuerySet [<Event: Harry Potter Leserunde>, <Event: Kafka Freunde>]>
>>>
>>> # auf dem qs Queryset nochmal einen Filter ausführen
>>> qs = qs.filter(name__contains='Har')
>>> qs
<QuerySet [<Event: Harry Potter Leserunde>]>
>>>
Um auf Foreign-Key-Beziehungen zuzugreifen, nutzen wir vor dem Attribut nochmal
den related_name
aus der Modelklasse und trennen ihn mit einem Double-Under
__
vom Attribut ab. Auf diese Weise lassen sich unbegrenzt Beziehungen abbilden.
Im Beispiel filtern wir Kategorie-Objekte anhand der Event-Namen:
>>> # alle Kategorien, die Events mit "Harry Potter" im Namen führen
>>> # wir nutzen hier den related name events aus dem Model Event
>>> qs = Category.objects.filter(events__name__contains="Harry Potter")
>>> qs
<QuerySet [<Category: Bücherwurm>]>
>>>
>>> # Aufgabe: alle Events mit "spo" im Namen der Kategorie
>>> qs = Event.objects.filter(category__name__contains="spo")
>>> qs
<QuerySet [<Event: Outdoor Boxen im Görlitzer Park>]>
>>> # Hypothetisches Selektieren aller Kategorien, deren Events Reviews
>>> # haben, die das Wort gut beinhalten:
qs = Category.objects.filter(events__reviews__review__contains="gut").distinct()
startswith / endswith
Äquivalent zu den Python String-Methoden startswith / endswith
>>> # Alle Events, die mit der Zeichenfolge "Ka" beginnen
>>> events = Event.objects.filter(name__startswith="Ka")
>>> events
<QuerySet [<Event: Kafka Freunde>]>
>>>
>>> # Events, die auf "e" enden
>>> events = Event.objects.filter(name__endswith="e")
>>> events
<QuerySet [<Event: Harry Potter Leserunde>, <Event: Kafka Freunde>]>
gt / gte / lt / lte
Arithmetische Vergleiche: grö0er, größer gleich, kleiner, kleiner gleich.
>>> # Alle Events mit ID größer als 40
>>> qs = Event.objects.filter(id__gt=40)
>>> qs
<QuerySet [<Event: Der Hobbit-Club>, <Event: Harry Potter Leserunde>,..]>
>>>
>>> # alle Kategorien, die Events mit einer ID größer als 45 haben
>>> qs = Category.objects.filter(events__id__gt=45)
>>> qs
<QuerySet [<Category: Bücherwurm>, <Category: Bücherwurm>, <Category: Sport>]>
>>>
>>> # die doppelten Einträge resultieren aus dem INNER JOIN, den Django
>>> # automatisch durchführt.
>>> str(qs.query)
>>>
>>> # mit einem distinct können wir die doppelten Einträge aber loswerden
>>> qs = Category.objects.filter(events__id__gt=45).distinct()
>>> qs
<QuerySet [<Category: Bücherwurm>, <Category: Sport>]>
year / month / day / hour
Datums-und Zeit Lookups.
>>> # Alle Events aus dem Jahr 2023
>>> Event.objects.filter(date__year=2023)
<QuerySet [<Event: Der Hobbit-Club>, <Event: Harry Potter Leserunde>, ...]>
>>>
>>> # Alle Events vor dem Jahr 2023
>>> Event.objects.filter(date__year__lt=2023)
<QuerySet []>
>>>
>>> # 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)
>>> qs = qs.filter(date__month=1).filter(name__icontains="Hobbit")
<QuerySet [<Event: Der Hobbit-Club>]>
IN - Lookup
>>> qs = Event.objects.filter(category__in=[6, 4, 5])
>>> qs
<QuerySet []>
Lazy Evaluation
Ein Queryset wird erst auf der Datenbank ausgeführt, wenn es auch angefordert wird, d.h. evaluiert wird. Man kann also bedenkenlos in mehreren Schritten Filter auf Querysets anwenden, ohne sich Gedanken um Performance oder ähnliches machen zu müssen.
Folgende Aktionen evaluieren ein Queryset unter anderem:
list(qs)
bool(qs)
len(qs)
Iteration über ein Queryset
Pickling eines Querysets
Slicing eines Querysets.
Auch das Aufrufen der Repräsentation des Querysets mit repr(qs)
evaluiert ein Queryset, deshalb sehen wir auf der Shell auch gleich ein Resultat.
Allerdings wird hier nur eine Submenge des Querysets evaluiert.
Mehr zu Lazy Querysets hier:
Caching
Ein Queryset wird gecached, wenn das gesamte Queryset evaluiert wird. Falls die Daten in der Datenbank durch eine andere Quelle (Software) verändert werden, wird das in dem bereits gecachten Queryset u.U. nicht berücksichtig.
Ein erneuter Aufruf von all()
auf dem bereits ausgeführten Queryset bringt dann die
gewünschten Daten.
Querysets speichern das Ergebnis intern in einem _result_cache
. Dieser ist
nur gefüllt, wenn das Queryset evaluiert wurde. Jede Methode, die nun auf dem
Queryset ausgeführt wird und kein NEUES Queryset Objekt erstellt, nutzt den
Cache, statt eine neue Datenbankanfrage zu machen.
>>> qs = Event.objects.all()
>>>
>>> # result cache ist leer, Queryset wurde noch nicht evaluiert.
>>> qs._result_cache
>>>
>>> # list evaluiert das Queryset
>>> list(qs)
[<Event: Act reach become blue.>, ...]
>>>
>>> # nun ist _result_cache gefüllt
>>> qs._result_cache
[<Event: Act reach become blue.>, ...]
>>> # wenn wir jetzt diesen Eintrag direkt in der Datenbank ändern
>>> # würde sich das im Queryset nicht auswirken:
>>> list(qs)
[<Event: Act reach become blue.>, ...]
>>> # Erst, wenn wir uns die Objekte mit all wieder selektieren,
>>> # finden wir die Änderung aus der Datenbank
>>> qs = Event.objects.all()
[<Event: Act reach become red.>, ...]
>>>
>>> # der result cache ist nur eine Liste
>>> type(qs._result_cache)
<class 'list'>
>>>
>>> # die sich sogar manipulieren ließe...
>>> qs._result_cache.append("Test")
>>> qs._result_cache[-1]
'Test'