die Model-Api

Wir wollen nun Kategorie- und Event-Objekte in die Datenbank eintragen. Um das nicht direkt in der Datenbank zu machen, was definitiv auch der falsche Weg wäre, machen wir das hier nun testweise in der Django-Shell.

Später werden diese Events freilich von Usern über Web-Formulare erstellt bzw. von uns als Testdatensatz mit einem Faker generiert. Wir öffnen nun also die Django Shell mit folgendem Befehl:

python manage.py shell

die Django Shell

Der Befehl python manage.py shell öffnet den interaktiven Python-Interpreter. Zusätzlich zu dem, was der Python Interpreter per default bietet, lädt die Django-Shell auch noch die projektspezifischen Settings, damit wir mit dem Projekt interaktiv agieren können.

Die Django-Shell eignet sich hervorrangend zum Testen kleiner Code-Schnipsel oder Abfragen. Für wiederkehrende administrative Aufgaben, wie zum Beispiel dem Erstellen von Reports oder Datenexporten, sollte der Weg über ein sogenanntens Management-Command genommen werden (siehe Kapitel Testdaten generieren, dort erstellen wir ein Management-Command zum Generieren von Testdaten).

Objekte erstellen

wir wollen nun erstmal eine Kategorie anlegen. Dazu importieren wir aus events das Kategorie- und das Event-Model.

Um ein Kategorie-Objekt zu erstellen, rufen wir den Konstuktor der Klasse Category mit den entsprechenden Keyword-Argumenten name und description auf. Damit existiert ein Objekt der Klasse Category, es ist allerdings noch nicht persistent in der Datenbank gespeichert. Um das Objekt dauerhaft zu speichern, gibt es die Methode save.

Hier ein Ausschnitt einer interaktiven Session in der Shell:

>>> from events.models import Category, Event

>>> # ein Objekt der Klasse Category anlegen
>>> sport = Category(name='Sport', description='fit and fun')
>>> type(sport)
<class 'events.models.Category'>
>>> # Auf Attribut name zugreifen
>>> sport.name
Sport
>>> # Das Objekt hat noch keine ID, denn die wird von der DB vergeben.
>>> sport.id
>>>
>>> # Um das Objekt persistent zu speichern, führen wir save() aus
>>> sport.save()
>>>
>>> # id und die beiden Zeitangaben stammen aus der Datenbank.
>>> D.h. das Objekt wurde tatsächlich in die Datenbank eingetragen.
>>> sport.id
9
>>> sport.created_at
datetime.datetime(2022, 1, 6, 11, 20, 14, 931188, tzinfo=<UTC>)
>>> sport.updated_at
datetime.datetime(2022, 1, 6, 11, 20, 14, 931188, tzinfo=<UTC>)

Der Befehl sport.__dict__ ist eine elegante Möglichkeit in Python, alle Attribute eines Objekts in Form eines Dictionaries auszugeben. Wer will, probiert es gleich mal aus!

Wir können jetzt mit dem sqlite-Browser bzw. der VSCode-Extension in der Tabelle events.category prüfen, ob eine Kategorie namens Sport eingetragen wurde. Das sollte der Fall sein.

../_images/sqlite_browser_category_1.png

Wir haben also ein Kategorie-Objekt mit den nötigen Keyword-Argumenten erstellt und dann mit save() persistent in unserer Entwicklungs-Datenbank gespeichert. Dazu waren zwei Schritte nötig.

Wir können ein Objekt auch in nur einem Schritt erstellen und zwar über den Manager der Kategorie-Klasse (mehr zu Managern später).

>>> books = Category.objects.create(name="Bücherwurm")
>>> books.id
11
>>> books.name
Bücherwurm
>>> # das Objekt verändern und speichern
>>> books.description = "Alles was mit Büchern zu tun hat"
>>> books.save()
>>> books.description
Alles was mit Büchern zu tun hat

Hinweis für Windows-Nutzer: Es kann sein, dass die Powershell mit dem falschen Zeichen-Encoding konfiguriert ist, und keine Umlaute darstellen kann. Dann entweder auf utf-8 umstellen oder die Git-Bash nehmen, falls zur Hand. Im Notfall kann man sich auch erstmal mit „ue“ behelfen. Die Objekte, die wir hier anlegen, sind alles nur Testobjekte, die später wieder rausgelöscht werden.

Event-Objekt erstellen

Wir wollen nun ein Event-Objekt erstellen. Um eine 1:n Beziehung auf die Tabelle Category zu realisieren, setzen wir das Attribut category auf das oben erstellte Kategorie-Objekt books.

Um ein Datum für das Event festzulegen, importieren wir timezone aus den Django Utils und timedelta aus dem datetime-Modul. Unser Events werden jetzt pauschal zwei Tage in der Zukunft stattfinden.

Später kann man über ein Formular natürlich einen gewünschten Zeitpunkt eingeben.

Hier ein Ausschnitt einer interaktiven Session in der Shell:

>>> from django.utils import timezone
>>> from datetime import timedelta
>>>
>>> d = timezone.now() + timedelta(days=2)
>>> d
datetime.datetime(2022, 1, 8, 12, 50, 11, 205027, tzinfo=<UTC>)
>>>
>>> e1 = Event.objects.create(name="Hitchhikers Club", date=d, category=books)
>>>
# Wenn wir das Objekt e1 anzeigen lassen, bekommen wir die
# String-Repräsentation des Objektes, wie im Model festgelegt
>>> e1
<Event: Hitchhikers Club>
>>>
>>> e1.date
datetime.datetime(2022, 1, 8, 12, 50, 11, 205027, tzinfo=<UTC>)
>>> type(e1)
<class 'events.models.Event'>

Bisher hatten wir uns nur einfache Felder wie String oder Datum des Eventobjekts ausgeben lassen. Was ist aber mit dem Feld category?

Jedes Event hat genau eine Category, und über das category Attribut können wir dieses Category-Objekt ansprechen. Es handelt sich hier also nicht nur um die numerische ID des Category-Objekts, sondern um das Category-Objekt selbst.

>>> # über e1.category haben wir Zugriff auf das Related Objekt.
>>> Nämlich ein Objekt der Klasse Category,

>>> # welches wir über den Foreign-Key adressieren
>>> e1.category
<Category: Bücherwurm>
# wenn wir die ID des related Objects haben wollen, können wir das mit
# dieser Syntax tun:
>>> e1.category_id
11
# category_id entspricht dem Datenbankfeld.
# alternativ ginge natürlich auch:
>>> e1.category.pk
11
>>> e1.category.id
11
>>>
>>> # mit dieser neuen Schreibweise kann man die Events auch direkt mit der
>>> # Category-ID anlegen
>>> e1 = Event.objects.create(name="Hitchhikers Club", date=d, category_id=11)
>>> # und sprechen jetzt über das Event-Objekt die Beschreibung der
>>> # Kategorie an
>>> e1.category.description
'Alles was mit büchern zu tun hat'
>>>
>>> # Wir können Objekte natürlich auch aus der Datenbank löschen
>>> e1.delete()
(1, {'events.Event': 1})
>>>
>>> # Das Objekt hitchhiker existiert zwar noch hier in der Shell, +
>>> # aber nicht mehr in der Datenbank
>>> e1
<Event: Hitchhikers Guide Club>
>>>
>>> # wir können das daran erkennen, dass das Objekt nun keine ID mehr besitzt
>>> e1.id
>>>
>>> # da wir das Objekt gleich noch benötigen, erstellen wir es nochmal
>>> e1 = Event.objects.create(name="Hitchhikers Club", date=d, category=books)
>>>
>>> e1.id
45

# und noch eine Leserunde
>>> e2 = Event.objects.create(name="Harry Potter Fans", date=d,  category=books)

Dem aufmerksamen User ist vielleicht aufgefallen, dass wir die ID eines Category-Objektes einmal mit e1.category.pk und einmal mit e1.category.id ausgegeben haben. Das Ergebnis war das selbe. Warum ist das so?

Wie wir gesehen hatten, erstellt Django für jedes Model hinter den Kulissen ein id-Feld. Wir können aber auch ein anderes Feld als id mit einem Primary Key versehen, wenn wir das wollten.

Intermezzo: Timezone

Generell stellt es vor allem in internationalen Projekten ein Problem dar, wenn eine Zeitangabe (datetime) nicht mit der Zeitzone gespeichert wird, in der es erstellt wurde. Deshalb ist es in international ausgerichteten Projekten sinnvoll, Daten im System immer als UTC (Universal Time Coordinated) zu speichern und in der Ausgabe dann darauf zu achten, dass die Zeit in der lokalen Zeitzone ausgegeben wird.

Führen wir in der Shell timezone.now() aus und formatieren das Ergebnis entsprechend, sehen wir, dass die Zeit eine Stunde vor unserer Zeit liegt. Deutschland liegt in der Zeitzone UTC+1 (bzw. UTC+2 in der Sommerzeit) und das erklärt den Unterschied.

>>> timezone.now().strftime("%y-%m-%d %H::%M::%S")

Python Objekt untersuchen

Wir hatten schon zwei Methoden gesehen, ein Python-Objekt zu untersuchen. Das war einmal die built-in Funktion dir() und einmal das __dict__-Attribut, welches uns den aktuellen Zustand eines Objekts zeigt. Eine weitere Methode, Informationen über ein Objekt zu erhalten, ohne einen Debugger zu nutzen, ist das Modul inspect.

from inspect import getmembers, isfunction
getMembers(e1)

# nur die Methoden des Objekts betrachten
functions = list(map(lambda x: x[0], getmembers(e1, isfunction)))

Um mit Objekte zu arbeiten, müssen wir diese aus der Datenbank laden und modifizieren können. Hier kommt jetzt der Manager ins Spiel.

der Model-Manager

Ein Manager ist eine Python-Klasse, die die Schnittstelle zwischen Datenbankabfragen und einem Django-Model bereitstellt. Jedes Model in Django besitzt mindestens einen Manager. Der Default-Manager eines Objektes heisst objects. Der Manager wird formal auch als Object Relation Mapper (ORM) bezeichnet.

einen eigenen Manager entwickeln

Später im Buch werden wir unseren eigenen Manager entwickeln. Ein Manager ist nichts weiter, als eine Pythonklasse, die uns eine Schnittstelle auf die Datenbank-Tabelle liefert. Damit lassen sich komfortabel immerwiederkehrende Aufgaben lösen.

Mehr zu Managers schon mal hier: https://docs.djangoproject.com/en/ref/topics/db/managers/

Wichtig: der Manager ist immer der Klasse und nicht dem Objekt zugeteilt. Das würde auch keinen Sinn machen, denn mit dem Manager hat man Zugriff auf die Datenbank-Tabelle und damit auf alle Einträge einer Klasse.

Gucken wir uns zwei Methoden des Managers mal genauer an: get und all. get liefert uns ein Objekt der Klasse zurück und all liefert uns alle Objekte der Klasse in einem Queryset zurück.

Hier ein Ausschnitt einer interaktiven Session in der Shell:

>>> # der Manager der Event-Klasse
>>> Event.objects
<django.db.models.manager.Manager object at 0x0000019EA592BE50>
>>>
>>> # der Manager der Category-Klasse
>>> Category.objects
<django.db.models.manager.Manager object at 0x0000019EA592BE50>
>>>
>>> # mit get holen wir uns ein Objekt der Klasse Event.
>>> # pk steht für Primary Key und ist die ID des Objekt
>>> harry = Event.objects.get(pk=46)
>>> harry.name
'Harry Potter Leserunde'
>>>
>>> # wir können jedes beliebige Attribut abfragen:
>>> harry = Event.objects.get(name="Harry Potter Leserunde")
>>>
>>> # oder auch mehrere
>>> harry = Event.objects.get(name="Harry Potter Leserunde", sub_title=None)
>>> harry
'Harry Potter Leserunde'
>>>
>>> # Falls ein Objekt nicht existieren sollte, wird eine Exception ausgelöst
>>> Event.objects.get(pk=3333)
events.models.Event.DoesNotExist: Event matching query does not exist.
>>>
>>> # über den Bezeichner harry haben wir jetzt wieder Zugriff auf das Objekt
>>>
>>> # wir ändern jetzt das Attribut name und speichern es in der DB ab.
>>> harry.name = "Harry Potter Lesezirkel"
>>> harry.save()

>>> # um uns alle Event Objekte anzeigen zu lassen, nutzen wir all()
>>> Event.objects.all()
<QuerySet [<Event: Harry Potter Lesezirkel.>, <Event: Hitchhikers Club>]
>>>
>>> # Wir legen jetzt noch einen weiteren Event an (gleiches Datum und Kategorie)
>>>
>>> Event.objects.create(name="Kafka Freunde", date=d, category=books)
>>>
>>> Event.objects.all()
<QuerySet [<Event: Harry Potter Lesezirkel.>, <Event: Kafka Freunde>, <Event: Hitchhikers Club>]