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'