Was ist Django?
Django ist ein „Batteries-included“-Web-Framework für Python. Während die meisten Frameworks von Ihnen verlangen, eine Datenbank-Schicht, eine Template-Engine, ein Admin-Panel und ein Authentifizierungssystem selbst auszuwählen, liefert Django all diese Komponenten in einem kohärenten Paket aus. Sie beschreiben Ihre Daten und Ihre Seiten, und das Framework übernimmt die mühsamen, sicherheitskritischen Teile.
Es wurde 2003 für die Redaktion einer Zeitung entwickelt – ein Team, das Features schnell ausliefern musste, ohne unter dem Zeitdruck zusammenzubrechen. Dieser Ursprung ist spürbar. Django setzt auf Konventionen, Explizitheit und die Einstellung „es sollte einen offensichtlichen Weg geben“. Dadurch ist es eine der zuverlässigsten Optionen für inhaltsreiche und datengesteuerte Websites geblieben.
Wenn Sie jemals eine Woche damit verbracht haben, ein ORM, ein Migration-Tool und ein Auth-System miteinander zu verknüpfen, dann ist Django das Framework, das Ihnen diese Woche erspart.
Das MTV-Pattern
Django bezeichnet seine Architektur als MTV: Model, Template, View. Es handelt sich dabei um dasselbe Konzept wie bei MVC, nur mit anderen Bezeichnungen, was viele Anfänger anfangs verwirrt.
- Ein Model ist eine Python-Klasse, die eine Tabelle und deren Zeilen beschreibt.
- Ein Template ist eine HTML-Datei mit Platzhaltern, die die Daten rendert.
- Eine View ist eine Funktion oder Klasse, die einen Request empfängt, Daten von den Models anfordert und eine Response zurückgibt.
Der Controller im klassischen MVC-Sinne ist Django selbst. Der URL-Dispatcher entscheidet, welche View ausgeführt wird, und das Framework vermittelt zwischen den drei Ebenen. Sobald dieses Schema klar ist, wird alles andere in Django zu einem Detail statt zu einem Rätsel.
Modelle und das ORM
Ein Modell ist eine Klasse, die von models.Model erbt. Jedes Attribut ist ein Feld, und jedes Feld wird zu einer Spalte.
# blog/models.py
from django.conf import settings
from django.db import models
class Post(models.Model):
title = models.CharField(max_length=200)
slug = models.SlugField(max_length=200, unique=True)
body = models.TextField()
author = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="posts",
)
published_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ["-created_at"]
def __str__(self):
return self.title
CharField benötigt ein max_length, da es auf einen begrenzten VARCHAR abgebildet wird. TextField ist unbegrenzt. auto_now_add=True setzt einen Wert nur bei der Erstellung, während auto_now=True bei jedem Speichervorgang aktualisiert wird. null steuert die Datenbank und blank steuert die Formularvalidierung; diese sind bewusst getrennt.
Beziehungen sind explizit:
ForeignKeyist eine Many-to-One-Beziehung.ManyToManyFielderstellt eine Join-Tabelle.OneToOneFieldist ein eindeutiger Foreign Key.
Das related_name legt den Reverse Accessor fest, sodass user.posts.all() von der anderen Seite aus funktioniert. Das on_delete-Argument ist erforderlich, da Django sich weigert zu raten, was mit den Kindern passieren soll, wenn ein Elternelement gelöscht wird.
Migrationen
Migrationen sind versionskontrollierte Dateien, die Änderungen an Ihrem Schema beschreiben. Sie schreiben das Model, Django schreibt das SQL.
python manage.py makemigrations blog
python manage.py migrate
makemigrations analysiert Ihre Models und erstellt eine Migration; migrate wendet ausstehende Migrationen auf die Datenbank an. Da die Migrationsdateien in Ihrem Repository liegen, erhält jede Umgebung dasselbe Schema und Änderungen können wie jeder andere Code reviewt werden. Bearbeiten Sie niemals eine bereits angewendete Migration – fügen Sie stattdessen eine neue hinzu.
Querysets und Beziehungen
Das ORM gibt Querysets zurück, die lazy sind: Sie werden erst ausgeführt, wenn Sie über sie iterieren, sie slicen oder auswerten. Diese Lazy-Ausführung ermöglicht es Ihnen, Filter zu verketten und erst bei der finalen Abfrage die Performance-Kosten zu tragen.
Post.objects.filter(status="PB").exclude(author=user).order_by("-published_at")[:10]
Nützliche Queryset-Methoden sind unter anderem filter, exclude, get, first, exists, count, values, values_list, annotate und aggregate. Mit F()-Ausdrücken können Sie auf eine Spalte in der Datenbank verweisen, und Q()-Objekte ermöglichen es Ihnen, OR-Bedingungen zu erstellen.
from django.db.models import Count, Q
published = Post.objects.filter(
Q(status="PB") | Q(published_at__isnull=False)
).annotate(comment_count=Count("comments"))
Hier spielt das ORM seine Stärken voll aus: Komplexe Abfragen bleiben in Python, werden sicher parametrisiert und lassen sich modular zusammensetzen.
Das N+1-Problem vermeiden
Der am häufigsten vorkommende Performance-Fehler in Django ist die N+1-Query. Dies passiert, wenn Sie innerhalb einer Schleife auf ein verknüpftes Objekt zugreifen, was pro Zeile eine zusätzliche Query auslöst.
# 1 query for posts, then 1 query per post for the author
for post in Post.objects.all():
print(post.author.username)
select_related fügt Single-Value-Relationen über Joins in die ursprüngliche Query ein, während prefetch_related eine zweite Query ausführt und die Ergebnisse für Multi-Value-Relationen in Python zusammenfügt.
# 1 query for posts joined with author, 1 for the comments
posts = Post.objects.select_related("author").prefetch_related("comments")
Nutzen Sie select_related bei Foreign Keys und One-to-One-Feldern sowie prefetch_related bei Many-to-Many- und inversen Relationen. Wenn sich eine Seite langsam anfühlt, zählen Sie mit der Django Debug Toolbar die Queries, bevor Sie andere Optimierungen vornehmen.
Views: Funktionen und Klassen
Ein View ist jedes aufrufbare Objekt (Callable), das einen Request entgegennimmt und eine Response zurückgibt. Funktionsbasierte Views sind der einfachste Einstiegspunkt.
# blog/views.py
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, render
from .models import Post
def post_list(request):
posts = Post.objects.filter(status="PB").select_related("author")
return render(request, "blog/post_list.html", {"posts": posts})
def post_api(request, slug):
post = get_object_or_404(Post, slug=slug)
return JsonResponse({"title": post.title, "body": post.body})
Klassenbasierte Views tauschen ein wenig Indirektion gegen Wiederverwendbarkeit ein. ListView, DetailView, CreateView, UpdateView und DeleteView implementieren die Standard-CRUD-Pattern, und Mixins ermöglichen es, Verhalten modular zusammenzusetzen.
from django.views.generic import ListView
from .models import Post
class PostListView(ListView):
model = Post
template_name = "blog/post_list.html"
context_object_name = "posts"
paginate_by = 10
def get_queryset(self):
return Post.objects.filter(status="PB").select_related("author")
Nutzen Sie funktionsbasierte Views für einmalige Logik und generische klassenbasierte Views für Standard-Listen- und Detailseiten. Es ist völlig normal und sinnvoll, beides innerhalb einer Codebase zu mischen.
URLs
Der URL-Dispatcher ordnet einen Pfad einer View zu. Die Patterns befinden sich in urls.py, wobei jede App ihre eigenen definieren kann.
# config/urls.py
from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("blog/", include("blog.urls")),
]
# blog/urls.py
from django.urls import path
from . import views
app_name = "blog"
urlpatterns = [
path("", views.post_list, name="post_list"),
path("<slug:slug>/", views.post_detail, name="post_detail"),
]
Converter wie <int:id> und <slug:slug> validieren und typisieren das erfasste Segment. Benennen Sie Ihre Routes immer und lösen Sie diese mit reverse() oder dem {% url %}-Tag auf, anstatt Pfade hart zu codieren. So bleiben Links auch nach einer Umbenennung funktionsfähig.
Templates
Templates sind HTML mit einer kleinen, bewusst eingeschränkten Sprache. Autoescaping ist standardmäßig aktiviert, was Sie vor XSS schützt.
{% extends "base.html" %}
{% block content %}
<h1>{{ post.title }}</h1>
<p>By {{ post.author.username }} on {{ post.published_at|date:"F j, Y" }}</p>
<div>{{ post.body|linebreaks }}</div>
{% endblock %}
{% extends %} ermöglicht Template-Vererbung, {% include %} teilt Fragmente und Filter wie date, linebreaks und default formatieren Werte. Es gibt keinen beliebigen Python-Code in Templates; wenn ein Template Logik benötigt, gehört diese Logik in den View oder das Model.
Formulare und Validierung
Das Formular-System von Django validiert Eingaben, rendert das HTML und schützt mittels eines CSRF-Tokens vor Manipulationen.
# blog/forms.py
from django import forms
from .models import Post
class PostForm(forms.ModelForm):
class Meta:
model = Post
fields = ["title", "slug", "body", "status", "published_at"]
widgets = {"body": forms.Textarea(attrs={"rows": 12})}
# blog/views.py
from django.shortcuts import redirect, render
from .forms import PostForm
def post_create(request):
if request.method == "POST":
form = PostForm(request.POST)
if form.is_valid():
post = form.save(commit=False)
post.author = request.user
post.save()
return redirect(post)
else:
form = PostForm()
return render(request, "blog/post_form.html", {"form": form})
Ein ModelForm leitet Felder und Validierungen aus dem Model ab, sodass beide niemals auseinanderlaufen. Clean-Methoden wie clean_slug ermöglichen benutzerdefinierte Regeln, und form.is_valid() sammelt alle Fehler, bevor die Datenbank angesprochen wird.
Das Admin-Interface
Das Admin-Interface ist das am meisten unterschätzte Feature von Django. Registrieren Sie ein Model und Sie erhalten eine durchsuchbare, filterbare und berechtigungsgesteuerte Oberfläche für Ihr Personal.
# blog/admin.py
from django.contrib import admin
from .models import Post
@admin.register(Post)
class PostAdmin(admin.ModelAdmin):
list_display = ["title", "author", "status", "published_at"]
list_filter = ["status", "created_at"]
search_fields = ["title", "body"]
prepopulated_fields = {"slug": ["title"]}
raw_id_fields = ["author"]
date_hierarchy = "published_at"
Das ist ein vollständiges internes Tool in nur einem Dutzend Zeilen. Für Content-Websites ist das Admin-Interface oft bereits das eigentliche Produkt. Es dient zudem als Sicherheitsbarriere: Personalberechtigungen, has_add_permission und has_change_permission steuern exakt, was jede Rolle tun darf.
Einstellungen und Apps
Ein Django-Projekt ist eine Sammlung von Apps. Jede App ist ein eigenständiges Paket mit eigenen Models, Views, Templates und Migrations. INSTALLED_APPS listet diese auf, und settings.py enthält die Konfiguration.
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"blog",
]
Halten Sie Einstellungen aus der Versionsverwaltung fern, sofern es sich um Secrets handelt. Lesen Sie diese aus Umgebungsvariablen aus, teilen Sie die Einstellungen pro Umgebung auf, wenn das Projekt wächst, und nutzen Sie django-environ oder pydantic-settings zum Parsen. Die Befehle django-admin startproject und startapp erstellen das Grundgerüst für beide Ebenen, sodass die Struktur immer konsistent bleibt.
Sicherheitsstandards
Die Standardeinstellungen von Django sind so gewählt, dass der sichere Weg auch der einfachste ist:
- CSRF-Token schützen Formulare, die den Zustand ändern; die middleware lehnt Anfragen ohne diese Token ab.
- SQL injection wird vermieden, da Querysets Werte parametrisieren.
- XSS wird minimiert, da Templates Variablen automatisch escapen, sofern Sie dies nicht explizit mit
|safedeaktivieren. - Passwörter werden mit einem modernen Algorithmus und konfigurierbaren Hashern gehasht.
- Clickjacking wird durch die
X-Frame-Options-middleware blockiert. - HTTPS wird in der Produktion über
SECURE_SSL_REDIRECT,SESSION_COOKIE_SECUREundCSRF_COOKIE_SECUREerzwungen.
Führen Sie python manage.py check --deploy aus, bevor Sie live gehen. Es markiert die Einstellungen, die Sie vergessen haben, und ist weitaus kostengünstiger als ein Security-Review.
APIs mit Django REST Framework erstellen
Für JSON APIs ist das Django REST Framework (DRF) die Standard-Ergänzung. Ein Serializer beschreibt, wie ein Model in JSON umgewandelt wird und zurück, während ein Viewset zusammen mit einem Router die CRUD-Endpunkte generiert.
# blog/serializers.py
from rest_framework import serializers
from .models import Post
class PostSerializer(serializers.ModelSerializer):
author = serializers.StringRelatedField()
class Meta:
model = Post
fields = ["id", "title", "slug", "author", "status", "published_at"]
# blog/api.py
from rest_framework import viewsets
from .models import Post
from .serializers import PostSerializer
class PostViewSet(viewsets.ModelViewSet):
queryset = Post.objects.select_related("author")
serializer_class = PostSerializer
lookup_field = "slug"
# blog/urls.py
from rest_framework.routers import DefaultRouter
from .api import PostViewSet
router = DefaultRouter()
router.register("posts", PostViewSet, basename="post")
urlpatterns = router.urls
Serializer validieren die Eingaben analog zu Forms, Viewsets bieten List-, Create-, Retrieve-, Update- und Delete-Funktionen quasi kostenlos an, und DRF kann OpenAPI-Schemas für Ihre API generieren. Das ORM von Django und die Konventionen von DRF sind ein bewährter Weg für produktive APIs.
Testing
Der Test-Runner von Django erstellt eine frische Test-Datenbank und stellt einen Client bereit, der die gesamte Request-Pipeline durchläuft.
# blog/tests.py
from django.contrib.auth import get_user_model
from django.test import TestCase
from django.urls import reverse
from .models import Post
class PostDetailTests(TestCase):
def setUp(self):
user = get_user_model().objects.create_user("ada", password="secret")
self.post = Post.objects.create(
title="Hello",
slug="hello",
body="First post",
author=user,
status=Post.Status.PUBLISHED,
)
def test_detail_page_renders(self):
response = self.client.get(reverse("blog:post_detail", args=["hello"]))
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Hello")
def test_missing_post_returns_404(self):
response = self.client.get(reverse("blog:post_detail", args=["nope"]))
self.assertEqual(response.status_code, 404)
TestCase kapselt jeden Test in eine Transaktion und führt einen Rollback durch, sodass die Tests isoliert bleiben und schnell ausgeführt werden. Verwenden Sie django.test.Client für Views, das ORM direkt für Models und pytest-django, wenn Sie pytest-Fixtures bevorzugen. Konzentrieren Sie sich darauf, das Verhalten und die Berechtigungen zu testen, nicht die Implementierungsdetails.
Best Practices
- Halten Sie Models auf Daten und kleine Domain-Methoden fokussiert; lagern Sie die Orchestrierung in Services oder Views aus.
- Führen Sie immer
select_relatedundprefetch_relatedaus, wenn ein Template auf Relations zugreift. - Nutzen Sie
ModelFormund DRF Serializer, damit die Validierung an einem zentralen Ort bleibt. - Benennen Sie jede URL und nutzen Sie Reverse-Lookups; hardcoden Sie Pfade niemals in Templates oder im Code.
- Fügen Sie Datenbank-Indizes für Spalten hinzu, nach denen Sie filtern oder sortieren.
- Speichern Sie Secrets in Umgebungsvariablen und führen Sie
check --deployvor dem Release aus. - Schreiben Sie Migrations als Code-Review-Artefakte und bearbeiten Sie niemals eine bereits angewendete Migration.
- Testen Sie die Request-Pipeline mit
Clientund die Domain-Logik direkt.
Häufige Fehler
- Über ein Queryset iterieren und innerhalb der Schleife auf Relationen zugreifen, was zu N+1 Queries führt.
null=Trueanstelle eines leeren Strings bei einem String-Feld verwenden und anschließend mitNone-Prüfungen kämpfen.- Business-Logik in Templates auslagern, weil der View zu überladen wirkte.
save()in einer Schleife aufrufen, anstattbulk_createoderbulk_updatezu nutzen.on_deletevergessen oderCASCADEwählen, ohne über den möglichen Datenverlust nachzudenken.DEBUG = Trueund ein hartcodiertesSECRET_KEYin der Production-Umgebung lassen.- Das Admin-Interface als Frontend für Endnutzer behandeln, anstatt es als Tool für Mitarbeiter zu betrachten.
- Abfragen mit
len(queryset)anstatt mit.count()durchführen, wodurch jede einzelne Zeile in den Arbeitsspeicher geladen wird.
Wie geht es weiter?
Django bietet dir den vollständigen, konventionellen Weg zu einer produktionsreifen Python-Web-App. Wenn du einen schlankeren Core und mehr Wahlmöglichkeiten bevorzugst, lies als Nächstes den Flask-Guide. Wenn du eine moderne async JSON API baust und die Validierung über Typen lösen möchtest, ist FastAPI die richtige Wahl. Egal wofür du dich entscheidest: Investiere in die zugrunde liegende Datenbank. Indexing, Transaktionen und Query Planning sind wichtiger als das Framework, und die Backend-Roadmap geht hier tiefer ins Detail. Der Guide zu REST APIs behandelt die Design-Regeln, die eine API benutzerfreundlich machen.