.. _queryset: .. index:: single: Abfragen single: SELECT single: Aggregate single: Annotationen single: Filter single: Caching single: Lazy Evaluation single: Queryset single: Manager single: Lookups single: Slicing 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``. .. admonition:: 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** ``_ **Queryset-Methoden, die kein Queryset zurückliefern** ``_ 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. .. code-block:: python >>> d = timezone.now() + timedelta(days=20) >>> Event.objects.create(name="Der Hobbit-Club", category=books, date=d") >>> >>> slam_date = timezone.now() + timedelta(days=60) >>> Event.objects.create(name="Poetry Slam", category=books, date=d") >>> >>> 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 , , ...]> >>> >>> type(qs) >>> >>> # 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] >>> >>> qs.first() >>> >>> qs.last() 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. .. code-block:: python :linenos: >>> result = Event.objects.all()[:2] >>> result , ]> >>> >>> str(result.query) 'SELECT ... FROM "events_event" ... ASC LIMIT 2' >>> result.filter(name__contains="Hobbit") AssertionError: 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: **__** 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. .. code-block:: python >>> # Event mit der ID 41 >>> events = Event.objects.all().filter(id__exact=41) >>> events ]> >>> >>> # 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 , ,..]> >>> >>> # alle Events mit Namen Kafka Freunde (caseinsensitive) >>> events = Event.objects.all().filter(name__iexact='kafka freunde') events ]> 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. .. code-block:: python >>> # 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. .. code-block:: python >>> events = Event.objects.all() >>> >>> # alle Events, die das Wort "lesen" in der Beschreibung haben >>> qs = events.filter(description__contains='lesen') >>> qs , ]> >>> >>> # auf dem qs Queryset nochmal einen Filter ausführen >>> qs = qs.filter(name__contains='Har') >>> qs ]> >>> 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: .. code-block:: python >>> # 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 ]> >>> >>> # Aufgabe: alle Events mit "spo" im Namen der Kategorie >>> qs = Event.objects.filter(category__name__contains="spo") >>> qs ]> >>> # 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 .. code-block:: python >>> # Alle Events, die mit der Zeichenfolge "Ka" beginnen >>> events = Event.objects.filter(name__startswith="Ka") >>> events ]> >>> >>> # Events, die auf "e" enden >>> events = Event.objects.filter(name__endswith="e") >>> events , ]> gt / gte / lt / lte ------------------------- Arithmetische Vergleiche: grö0er, größer gleich, kleiner, kleiner gleich. .. code-block:: python >>> # Alle Events mit ID größer als 40 >>> qs = Event.objects.filter(id__gt=40) >>> qs , ,..]> >>> >>> # alle Kategorien, die Events mit einer ID größer als 45 haben >>> qs = Category.objects.filter(events__id__gt=45) >>> qs , , ]> >>> >>> # 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 , ]> year / month / day / hour ----------------------------- Datums-und Zeit Lookups. .. code-block:: python >>> # Alle Events aus dem Jahr 2023 >>> Event.objects.filter(date__year=2023) , , ...]> >>> >>> # Alle Events vor dem Jahr 2023 >>> Event.objects.filter(date__year__lt=2023) >>> >>> # 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") ]> IN - Lookup ----------------------------- .. code-block:: python >>> qs = Event.objects.filter(category__in=[6, 4, 5]) >>> qs 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. .. code-block:: python >>> qs = Event.objects.all() >>> >>> # result cache ist leer, Queryset wurde noch nicht evaluiert. >>> qs._result_cache >>> >>> # list evaluiert das Queryset >>> list(qs) [, ...] >>> >>> # nun ist _result_cache gefüllt >>> qs._result_cache [, ...] >>> # wenn wir jetzt diesen Eintrag direkt in der Datenbank ändern >>> # würde sich das im Queryset nicht auswirken: >>> list(qs) [, ...] >>> # Erst, wenn wir uns die Objekte mit all wieder selektieren, >>> # finden wir die Änderung aus der Datenbank >>> qs = Event.objects.all() [, ...] >>> >>> # der result cache ist nur eine Liste >>> type(qs._result_cache) >>> >>> # die sich sogar manipulieren ließe... >>> qs._result_cache.append("Test") >>> qs._result_cache[-1] 'Test' Weiterführende Links ------------------------ * ``_ * ``_