Die Models der App Events
Die App, die wir gerade erstellen, wird eine Event-Verwaltung sein. Wir benötigen ein Model
für den Event
selbst und ein Model für die Event-Kategorien
. Später werden wir diese Models noch ausbauen und weitere Models, zb. Reviews
, hinzufügen.
Beispiel für einen Event
Harry Potter Leserunde
in Kategorie: Bücherecke
wann: 12.0stable022, 18:00
wo: London Kings Cross Bahnhof
Was ist ein Model?
A model is the single, definitive source of information about your data. It contains the essential fields and behaviors of the data you’re storing. Generally, each model maps to a single database table.
—Django Dokumentation, Models
Ein Model ist im Grunde erstmal nur eine Pythonklasse, der diverse Attribute
und Methoden zugeteilt werden. Diese Klasse muss allerdings immer von models.Model
erben,
um zu funktionieren.
Diese so erzeugte Klasse dient Django später unter anderem als Erzeugungsvorschrift für das Erstellen
einer Datenbank-Tabelle, die dem Model entspricht. D.h. unser Model Category
,
welches wir gleich erstellen werden, wir in einem späteren Schritt auch als
Datenbank-Tabelle erzeugt. Softwaretechnisch gesehen handelt es sich bei einem Model um ein Geschäftsobjekt
.
Diesem Geschäftsobjekt können wir Geschäftsregeln
und Fachfunktionen
zuordnen.
Jede Model-Instanz entspricht einem Datensatz, „dh“. einer Zeile, in der entsprechenden Datenbank-Tabelle.
Die Attribute, die wir dem Model zuteilen, entsprechen später den
Datenbank-Feldern und damit den Datentypen der Datenbank. So wird aus
models.CharField(max_length=100)
in der Datenbank zum Beispiel ein VarChar 100
.
Mehr zu den Django-Feldtypen hier: https://docs.djangoproject.com/en/stable/models/fields/
Der Django Object-Relation-Mapper
kümmert sich selbständig um die
Konvertierung der Python-Datentypen in die entsprechenden DB-Datentypen und
zurück. Wir als Entwickler müssen diese Arbeit also nicht mühseliger selber machen.
Der ORM
ist datenbank-unabhängig, wir können also jede beliebige, von Django unterstützte relationale Datenbank nutzen,
ohne unseren Code verändern zu müssen.
Das Category-Model
Wir wollen jetzt zwei Python-Klassen anlegen, die in Django als Models bezeichnet werden:
Das UML-Klassendiagramm für die Category und den Event sieht in etwa so aus:
Das Category-Model soll die Kategorien beschreiben, denen später Events zugeordnet werden. Beispiele für eine Kategorie sind Sport oder Kochen.
Wir modifizieren die Datei event_manager/events/models.py
wie folgt:
from django.db import models
class Category(models.Model):
"""Eine Kategorie für einen Event."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
name = models.CharField(max_length=100)
sub_title = models.CharField(max_length=200, null=True, blank=True)
description = models.TextField(null=True, blank=True)
def __str__(self):
return self.name
Eine Kategorie hat zwingend einen Namen, z.B. „Sport“. Sub-Titel und
Beschreibung bleiben aber erstmal optional (null=True und
blank=True)
ermöglicht.
Es gehört zur Good Practice, dass die __str__()
-Methode, dh. die
String-Repräsentation des Objektes, implementiert wird.
Das machen wir hier, in dem wir einfach den Namen der Kategorie zurückgeben.
blank und null
Wenn ein neues Datenbank-Model definiert wird, sind null und blank per default auf false gesetzt. Sie unterscheiden sich wie folgt:
null
ist datenbank-bezogen: wenn ein Feld null=True gesetzt ist, kann es einen Datenbank-Eintrag als NULL (kein Wert) speichern. Bei null=false (default) muss zwingend ein Wert eingegeben werden (mandatory).blank
hingegen ist nötig für die spätere Eingabevaldierung über Formulare. Wenn blank=True ist, kann dieser Wert in einem Forular später ausgelassen werden, ansonsten ist es ein Pflichtfeld.
Meist treten blank und null im Doppelpack auf, denn wenn ein Feld nicht zwingend in der Datenbank eingetragen werden muss (null=True)
, sollte es auch im Eingabeformular nicht zwingend einzugeben sein (blank=True)
.
Um ein Model zu nutzen, muss es als Tabelle in einer Datenbank angelegt werden. Das machen wir aber später im Kapitel Migrationen.
Feldtypen
Wir haben bisher zwei Feldtypen gesehen: das models.CharField
, welches ein
Eingabefeld darstellt und in der Datenbank als VARCHAR
hinterlegt wird, und
das models.TextField
, welches ein Textfeld darstellt, und in der Datenbank
als TEXT
abgebildet wird. Jedem Feld übergeben wir noch weitere Argumente,
wie max_length
oder null
, um es weiter zu definieren.
Feldtyp |
Bedeutung |
---|---|
models.IntegerField |
Ganze Zahlen |
models.TextField |
Texte |
models.DateTimeField |
Datum-Zeit |
models.DateField |
Datum |
models.BigIntegerField |
Feld für sehr große Ganzzahlen |
models.PositiveIntegerField |
nur positive Zahlen |
models.BooleanField |
Boolsches Feld |
models.EmailField |
gültige Email Adresse |
models.FileField |
Datei-Referenz |
weitere Feldtypen finden sich hier:
https://docs.djangoproject.com/en/stable/models/fields/#field-types
Und was ist mit einer Id als Primary Key?
Dem einen oder anderen wird vielleicht aufgefallen sein, dass wir überhaupt kein ID-Feld für unser Category-Model angelegt haben. Für die Arbeit mit relationalen Datenbanken sind IDs aber unverzichtbar, um Objekte zu identifizieren und über Referenztabellen zu addressieren.
Die gute Nachricht ist: Django legt für uns hinter den Kulissen für jedes Model die entsprechende Spalte mit dem entsprechenden Datentypen an.
id = models.BigAutoField(primary_key=True)
Falls man allerdings doch ein eigenes Feld als Primary Key angeben wollte,
könnte man das bei der Feld - Definition im Model machen, in dem man die
Eigenschaft primary_key
auf True setzt:
key = models.IntegerField(primary_key=True)
Django untersützt übrigens das Arbeiten mit zwei Primary Keys nicht (composite primary keys).
Das Event-Model
Das Event-Model soll die einzelnen Events beschreiben. Beispiele für eine Event sind Leserunde am Abend oder Chili kochen für Programmierer. Jeder Event ist einer Kategorie zugeteilt, zum Beispiel der Kategorie Sport.
1:n-Beziehung
Die Beziehung zu dem Kategorie-Model ist eine 1:n-Beziehung
. Ein Event hat genau
eine Kategorie, aber jeder Kategorie können viele Events zugeordnet werden.
Diese Art von Beziehung wird in Django mit einem ForeignKey-Feld
gelöst,
welches auf die ID der Kategorie zeigt. Streng genommen handelt es sich um
eine 1:CN
- Beziehung, da es durchaus vorkommen kann, dass manchen
Kategorien kein Event zugeteilt wurde.
Der Event hat also einen ForeignKey
auf das Category-Model
. Weitere
Beziehungen sind die 1:1-Beziehung
und die n:m-Beziehung
, die wir später kennenlernen.
Wir modifizieren die Datei event_manager/events/models.py
und fügen ein weiteres Model ein:
class Event(models.Model):
"""Der Event, der auf einen bestimmten Zeitpunkt terminiert ist."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
name = models.CharField(max_length=100)
description = models.TextField(null=True, blank=True)
date = models.DateTimeField()
sub_title = models.CharField(max_length=200, null=True, blank=True)
category = models.ForeignKey(
Category, on_delete=models.CASCADE, related_name="events"
)
is_active = models.BooleanField(default=True)
class Meta:
ordering = ["name"]
verbose_name_plural = "Events"
def __str__(self):
return self.name
Hier ist eine Menge passiert! Wir haben die Klasse Event
angelegt und
ebenso wie schon die Klasse Category
von models.Model
erben lassen.
In der Meta-Klasse
definieren wir, dass die Default-Sortierung der Events
nach deren Name-Attribut sein soll, also aufsteigend alphabetisch. Der verbose_name_plural
bedeutet, wie die Events in der Administrationsoberfläche bezeichnet werden.
Dazu später mehr.
class Meta:
ordering = ["name"]
verbose_name_plural = "Events"
Dann haben wir einige Felder wie name
oder description
angelegt. Das
date-Feld
ist ein DateTime-Feld, welches einen genauen Zeitstempel
erwartet.
Die 1:n-Beziehung
zur Kategorie haben wir durch das category-Feld
gelöst, welches ein ForeignKey-Feld
ist.
category = models.ForeignKey(
Category, on_delete=models.CASCADE, related_name="events"
)
Ganz am Ende finden wir noch is_active
, ein Boolean-Field, dass angibt, ob ein Event gerade aktiv ist oder deaktiviert wurde.
Abgelaufene oder stornierte Events können somit später deaktiviert oder gelöscht werden. Per
default sind eingestellte Events erstmal aktiv, das wäre in einer moderierten
Umgebung sicherlich nochmal anders.
is_active = models.BooleanField(default=True)
Website-Besucher können diese Events später einstellen und editieren. Bisher wurde auf eine Zuordnung auf einen User aber noch verzichtet.
Mehr dazu im nächsten Kapitel Das User-Model anpassen.
Löschen eines referenzierten Objekts (on_delete)
Wenn eine Kategorie gelöscht wird, auf die wir vom Event per ForeignKey
referenzieren, müssen wir per on_delete
festlegen, was mit dem Event in diesem Fall
passieren soll. Django bietet hier unter anderem auch die von SQL bekannten
möglichkeiten:
|
lösche auch alle Events die dieser Kategorie zugeteilt sind |
|
verbiete das Löschen der Kategorie solange noch ein Event darauf referenziert. |
|
setzt den Foreign-Key auf NULL. Dazu muss das Feld nullable gesetzt sein (null=True) |
|
setzt das Foreign-Key Feld auf einen Defaultwert der im Feld per |
Weitere Möglichkeiten finden sich in der Django-Dokumentation:
https://docs.djangoproject.com/en/stable/models/fields/#django.db.models.ForeignKey.on_delete
So sieht unsere Datei event_manager/events/models.py
nun aus:
from django.db import models
class Category(models.Model):
"""Eine Kategorie für einen Event."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
name = models.CharField(max_length=100)
sub_title = models.CharField(max_length=200, null=True, blank=True)
description = models.TextField(null=True, blank=True)
def __str__(self):
return self.name
class Event(models.Model):
"""Der Event, der auf einen bestimmten Zeitpunkt terminiert ist."""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
name = models.CharField(max_length=100)
description = models.TextField(null=True, blank=True)
date = models.DateTimeField()
sub_title = models.CharField(max_length=200, null=True, blank=True)
category = models.ForeignKey(
Category, on_delete=models.CASCADE, related_name="events"
)
is_active = models.BooleanField(default=True)
class Meta:
ordering = ["name"]
verbose_name_plural = "Events"
def __str__(self):
return self.name
DB Index
Per default wird in Django nur der Primärschlüssel
von der Datenbank indiziert. Das bedeutet, dass Suchen mit dem
Primärschlüssel optimiert werden.
Falls wir wissen, dass ein anderes Feld einer Tabelle ebenfalls besonders
gerne und oft gesucht wird, können wir mit db_index
einen Datenbank-Index setzen.
age = models.IntegerField(db_index=True)
Man muss nur beachten, dass ein Index erst bei ein paar Tausend Zeilen Sinn macht und das beim Setzen des Indizes die Datenbanktabelle gesperrt ist. In dieser Zeit können keine Update- und Insert-Operationen auf der Tabelle ausgeführt werden.
Abstrakte Base Klassen
Wir haben in unserem Code noch einen kleinen Schönheitsfehler. Sowohl die
Category- wie auch das Event-Model beinhalten Felder, die in beiden Modellen
vorkommen. Zum Beispiel das Feld created_at
und updated_at
.
Nicht alle Models müssen Datenbank-Tabellen erzeugen. Wir können auch Klassen schreiben, die nur als abstrakte Klassen dienen, um sie zum Beispiel in mehreren Models zu nutzen. Die in der abstrakten Klasse definierten Felder werden dann in der geerbten Klasse als tatsächliche Datenbankfelder angelegt.
Um ein Model zu einem abstrakten Model zu machen, muss die Modelklasse das Attribut
abstract
in ihrer Meta-Klasse auf True
gesetzt haben.
Wir fügen der Datei event_manager/events/models.py
das folgende Mixin hinzu:
from django.db import models
class DateMixin(models.Model):
"""eine abstrakte Klasse, die selbst keine DB-Tabelle erstellt"""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
Um das Mixin zu nutzen, müssen unsere anderen Models nur noch davon erben.
Dadurch, dass das Category-Model sowie das Event-Model von DateMixin erben, verfügt es nun auch
über ein created_at
und ein updated_at
Feld.
Unsere Models sehen jetzt so aus.
from django.db import models
class DateMixin(models.Model):
"""eine abstrakte Klasse, die selbst keine DB-Tabelle erstellt"""
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
abstract = True
class Category(DateMixin):
"""Eine Kategorie für einen Event."""
name = models.CharField(max_length=100)
sub_title = models.CharField(max_length=200, null=True, blank=True)
description = models.TextField(null=True, blank=True)
def __str__(self):
return self.name
class Event(DateMixin):
"""Der Event, der auf einen bestimmten Zeitpunkt terminiert ist."""
name = models.CharField(max_length=100)
description = models.TextField(null=True, blank=True)
date = models.DateTimeField()
sub_title = models.CharField(max_length=200, null=True, blank=True)
category = models.ForeignKey(
Category, on_delete=models.CASCADE, related_name="events"
)
is_active = models.BooleanField(default=True)
class Meta:
ordering = ["name"]
verbose_name_plural = "Events"
def __str__(self):
return self.name
Es spricht nichts dagegen, dass ein Model mehrere Mixins erbt. So lassen sich
auf einfache Weise immer wiederholende Felder bequem auslagern. Denkbare wäre
zum Beispiel noch ein NameDescription-Mixin
oder ähnliches.
TIP: Die Mixins müssen nicht in der Datei models.py
liegen. Es kann
durchaus Sinn machen, gerade im Fall von immer wieder genutzten Feldern, diese
beispielsweise unter event_manager/event_manager/mixins.py
auszulagern und
sich dann in den Code wie folgt zu importieren:
from event_manager.mixins import DateMixin
Mehr zu abstrakten Base-Classes in der Doku: https://docs.djangoproject.com/en/ref/topics/db/models/#abstract-base-classes