Eigene Methoden am Model definieren

Wiederkehrende oder komplexe Operationen mit dem ORM sollten nicht mehrfach im Code implementiert werden. Stattdessen kapselt man solche Logik zentral, um das Don’t Repeat Yourself-Prinzip einzuhalten und die Wartbarkeit zu erhöhen.

Grundsätzlich kann Logik an verschiedenen Stellen liegen, z. B. in Views, Utility-Funktionen, in einem Manager oder in einem separaten Service-Layer. Entscheidend ist, welche Art von Logik vorliegt:

  • Domänenlogik (Business-Logik, die direkt ein einzelnes Objekt betrifft) gehört ins Model

  • Query-/Datenzugriffslogik (z. B. komplexe Filter oder QuerySets) gehört in einen Manager

  • Anwendungslogik (z. B. Orchestrierung mehrerer Models oder externer Systeme) gehört in einen Service-Layer

Methoden am Model beschreiben also das Verhalten eines einzelnen Objekts und sind überall dort verfügbar, wo eine Instanz dieses Models vorliegt.

Fat Model, Manager oder Service-Layer?

Häufig stellt sich die Frage, wo Logik am besten platziert wird.

  • Ein Fat Model-Ansatz bündelt domänenspezifische Logik direkt im Model.

  • Ein Manager kapselt wiederverwendbare Query-Logik und komplexe Datenbankabfragen.

  • Ein Service-Layer organisiert Abläufe, die mehrere Models oder Prozesse betreffen.

Wichtig ist die klare Trennung der Verantwortlichkeiten:

  • Model: Verhalten eines einzelnen Objekts

  • Manager: Zugriff und Filterung von Daten

  • Service-Layer: Koordination von Anwendungslogik

Views sollten möglichst dünn bleiben und hauptsächlich für die Verarbeitung von Requests und Responses zuständig sein.

Events in der Vergangenheit

Wir wollen jetzt noch eine weitere Methode in unserem Model implementieren, und zwar diesemal als property. Wir wollen prüfen, ob ein Event in der Vergangenheit liegt. Das könnte zum Beispiel nützlich sein, wenn wir nur Events anzeigen wollen, die in der Zukunft liegen. Oder ein asynchroner Task im Hintergrund laufend prüft, ob ein Event veraltet ist und gelöscht werden sollte.

Was ist eine property?

Eine property ist eine pythonische Art, Getter und Setter in der objektorientierten Programmierung zu verwenden. Python bietet uns einen eingebauten @property-Dekorator, der die Verwendung von getter und setter in der objektorientierten Programmierung vereinfacht. Eine gute Erklärung zu dem Konzept findet sich auf realpython.com: https://realpython.com/python-property/

Ändern wir nun das Event-Model in event_manager/events/models.py ab und implementieren die Methode has_finished. Da wir die Methode mit dem @property-Dekorator dekoriert haben, können wir diese später ohne Klammern aufrufen.

from django.utils import timezone

[..]

@property
def has_finished(self) -> bool:
    """Wenn das Event in der Vergangenheit liegt, return True."""
    now = timezone.now()
    return self.date <= now

Nun können wir das Ergebnis auf der Shell ausprobieren:

>>> some_event = Event.objects.last()
>>> some_event.has_finished
>>> False

Unsere event_manager/events/models.py sieht jetzt so aus:

from django.db import models
from django.utils import timezone
from django.contrib.auth import get_user_model

User = get_user_model()


class DateMixin(models.Model):
    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, unique=True)
    sub_title = models.CharField(max_length=200, null=True, blank=True)
    description = models.TextField(null=True, blank=True)

    class Meta:
        ordering = ["name"]
        verbose_name = "Kategorie"
        verbose_name_plural = "Kategorien"

    def __str__(self):
        return self.name


class Event(DateMixin):
    """Der Event, der auf einen bestimmten Zeitpunkt terminiert ist."""

    class Group(models.IntegerChoices):
        SMALL = 2, "kleine Gruppe"
        MEDIUM = 5, "mittelgroße Gruppe"
        BIG = 10, "große Gruppe"
        LARGE = 20, "sehr große Gruppe"
        UNLIMITED = 0, "ohne Begrenzung"

    name = models.CharField(max_length=100, unique=True)
    description = models.TextField(null=True, blank=True)
    date = models.DateTimeField()
    sub_title = models.CharField(max_length=200, null=True, blank=True)
    is_active = models.BooleanField(default=True)
    min_group = models.IntegerField(choices=Group.choices)
    category = models.ForeignKey(
        Category, on_delete=models.CASCADE, related_name="events"
    )
    author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="events")

    class Meta:
        ordering = ["name"]

    def __str__(self):
        return self.name

    def related_events(self):
        """
        Ähnliche Events aus der gleichen Kategorie und der selben
        min-group.
        """
        related_events = Event.objects.filter(
            min_group=self.min_group,
            category=self.category,
        )
        return related_events.exclude(pk=self.id)

    @property
    def has_finished(self) -> bool:
        """Wenn das Event in der Vergangenheit liegt, return True."""
        now = timezone.now()
        return self.date <= now

Was gehört als Methode in das Model?

Oft stellt sich dem Entwickler die Frage, welche Methoden im Model definiert werden und welche an anderer Stelle implementiert werden sollen. Als groben Ansatz kann man sagen, dass jede Methode, die genau eine Instanz des Models, also eines Objekts, betrifft, im Model definiert wird.

Das trifft auch dann zu, wenn die Rückgabe dieser Funktion eine Menge repräsentiert, wie in der Methode related_events, die als Rückgabewert ein Queryset hat.

Funktionen und Methoden, die nicht ein Objekt konkret betreffen, zum Beispiel Submengen von Objekten, sollte eher in eigene Manager oder Views ausgelagert werden. Funktionen, die sich im Grunde auf gar kein Objekt beziehen, können zum Beispiel in einem Service-Layer abgelegt werden. Zum Beispiel das Anfragen an eine entfernte API oder ähnliches.

Hypothetisch könnte das so aussehen: unter event_manager/events/services.py tragen wir eine Klasse ein, die eine entfernte API anspricht und die wir in der Event-App benötigen.

class ServiceAPI:
    def __init__(self):
        ...

In den Views könnte man diese Klasse importieren und nutzen. Nicht jede Klasse, die in Django verwendet wird, muss also zwingend ein Model sein!

from .services import ServiceAPI

def my_view(request):
    """Eine Beispiel-View ohne Aufgabe."""
    api = ServiceAPI()
    ...

Der Django Style Guide

Die Reihenfolge der Methode und Attribute in dem Django-Model ist nicht völlig willkürlich, sondern orientiert sich an dem Django Styleguide, der unter https://docs.djangoproject.com/en/dev/internals/contributing/writing-code/coding-style/ einzusehen ist. Hier wird unter anderem auch die Reihenfolge der Methoden und Attribute in einem Django-Model angegeben.

  • an erster Stelle kommen die Attribute (Datenbank-Felder)

  • die Angabe der Manager (falls vorhanden)

  • die Meta-Klasse (falls benötigt)

  • die String-Repräsentation __str__()

  • die save() - Methode (falls benötigt)

  • die get_absolute_url() - Methode

  • eigene Methoden

Die Einhaltung der Reihenfolge sollte man sich mittelfristig einprägen und einhalten, vor allem die Reihenfolge der Methodendefinitionen.

Generell bietet der Styleguide noch weitere interessante Informationen. So ist zum Beispiel die präferierte Zeilenlänge in Django 88 Zeichen und nicht 79, wie das zum Beispiel in der PEP8 vorgeschlagen wird.

Beim Einsatz von Lintern wie Flake8 oder Formattern wie black oder autopep8 sollte man seine Einstellungen so anpassen, falls man sich an dem Styleguide orientieren möchte.

Code-Qualität mit Ruff

Moderne Projekte nutzen das Tool ruff der Firma astral, das sowohl Linting als auch Formatting übernimmt und sich gut anpassen lässt. ruff ist ein sehr schneller Linter und Formatter für Python, der viele Tools wie Flake8 oder isort ersetzt.

Installiert wird er als Entwicklungsabhängigkeit mit uv:

uv add --dev ruff

Anschließend kann ruff über die Kommandozeile ausgeführt werden:

uv run ruff check .

Für automatische Formatierung des Codes:

uv run ruff format .

Damit lassen sich sowohl Stilprobleme erkennen als auch der Code direkt vereinheitlichen.