.. _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: -------------------------------------- * ``_ * ``_