.. _event_models:
.. index::
single: Model
single: Event-Verwaltung
single: models.Model
single: Kategorie
single: Django-Feldtypen
single: abstrakte Klassen
single: Datentypen
single: Datenbank-Index
single: null
single: blank
single: Mixin
single: on_delete
single: ForeignKey
single: 1:n-Beziehung
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: ``_
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.
.. image:: /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:
.. image:: /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:
.. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/models/events/0_events.py
:language: python
:lines: 1-15
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.
.. admonition:: 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.
.. csv-table:: Wichtige Feldtypen in Django
:header: "Feldtyp", "Bedeutung"
:widths: 40, 40
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:
----------------------------------------
``_
.. admonition:: 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.
.. code-block:: python
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:
.. code-block:: python
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*.
.. admonition:: 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:
.. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/models/events/0_events.py
:language: python
:lines: 15-34
**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.
.. code-block:: python
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.
.. code-block:: python
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.
.. code-block:: python
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**.
.. admonition:: related_name-Attribut
Wir können die Events später auch über das Kategorie-Objekt addressieren.
Um den Zugriff zu erleichtern, geben wir dieser Referenzierung einen Namen.
Das ist im Zweifel immer der kleingeschriebene Plural des referenzierenden
Models, hier also ``events``.
Mehr zum ``related_name``-Attribut findet sich natürlich wieder in der
Django-Doku:
``_
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:
.. csv-table:: on_delete Möglichkeiten
:widths: 40, 40
``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:**
``_
So sieht unsere Datei :file:`event_manager/events/models.py` nun aus:
.. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/models/events/0_events.py
:language: python
.. admonition:: 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.
.. code-block:: python
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.
.. image:: /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:
.. code-block:: python
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.
.. rli:: https://raw.githubusercontent.com/realcaptainsolaris/event_manager_code/main/models/events/1_events.py
:language: python
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:
.. code-block:: python
from event_manager.mixins import DateMixin
**Mehr zu abstrakten Base-Classes in der Doku:**
``_
Weiterführende Links zum Thema Models:
--------------------------------------
* ``_
* ``_