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.

../_images/erd_1.png

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:

../_images/uml_class_1.png

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.

Wichtige Feldtypen in Django

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).

https://en.wikipedia.org/wiki/Composite_key

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:

on_delete Möglichkeiten

CASCADE

lösche auch alle Events die dieser Kategorie zugeteilt sind

PROTECT

verbiete das Löschen der Kategorie solange noch ein Event darauf referenziert.

SET_NULL

setzt den Foreign-Key auf NULL. Dazu muss das Feld nullable gesetzt sein (null=True)

SET_DEFAULT

setzt das Foreign-Key Feld auf einen Defaultwert der im Feld per default angegeben sein muss.

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.

../_images/uml_class_2.png

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