Code First ohne Schmerzen: Migrationen, die Produktion nicht umwerfen
Code First ist bequem, solange Modell, Migration und Produktionsdatenbank im selben Takt bleiben. So planst du Entitäten, Indizes, Migrationen, Azure-Deployments und Nachkontrollen für vorhersehbare Änderungen.
Der vollständige Artikel steht unten. Aktivieren Sie JavaScript für Quiz und Simulationen.
Produktionslektion • EF Core • Azure • SQL
Code First tut nicht weh, wenn eine Migration eine Entscheidung statt Zufall ist.
Code First ist bequem, wird aber schnell riskant, wenn wir die Datenbank als automatische Nebenwirkung des C#-Modells behandeln. Dieser Beitrag zeigt den Prozess: Modell, Migration, Indizes, Deployment, Kontrollkonsole und Abnahme.
Meist beginnt es harmlos. Jemand ergänzt eine Funktion: Kundenbewertungen, Moderationsstatus, Ereignishistorie, Zahlung oder ein Flag im Adminbereich. Lokal funktioniert alles. Das Formular speichert, die Liste aktualisiert sich, der Button veröffentlicht. Erst Produktion zeigt, ob wir ein System gebaut haben oder nur ein Formular mit Tabelle.
In einem gut geführten Projekt ist die Datenbank kein Ort, an dem „irgendwie gespeichert wird“. Sie ist eine Vereinbarung zwischen Code, Nutzern, Administration, Backup, Datenschutz und künftigen Änderungen. Code First kann hervorragend sein, solange es das Nachdenken über Daten nicht ersetzt.
$ dotnet ef migrations add CustomerReviews
✓ Modell-Snapshot aktualisiert
$ dotnet ef migrations script --idempotent -o db.sql
✓ Idempotentes Skript erzeugt
$ az webapp deployment slot swap --slot staging
• Smoke-Tests laufen: API, Admin, öffentlicher Blog
✓ Release abgenommen ▌
Szenario: ein kleines Modul berührt Produktion
Wir ergänzen Kundenbewertungen. Nutzer hinterlassen Namen, Unternehmen, Bewertung und Kommentar. Der Beitrag erscheint erst nach Moderation öffentlich. Die E-Mail dient nur zur Verifizierung und darf nicht sichtbar sein. Im Adminbereich wird angenommen oder abgelehnt.
Auf den ersten Blick sind es wenige Felder. Tatsächlich geht es um Datenschutz, Indizes, Zustände, Spamabwehr, Validierung, Adminabläufe und das Verhalten des Systems in sechs Monaten.
Schlecht
Die Tabelle bildet das Formular ab: E-Mail, Text und Bewertung. Veröffentlichung ist ein Boolean. Wochen später kommen Moderation, Quelle, Spamsperre und Änderungshistorie hinzu. Das Modell wächst ohne Plan.
Gut
Die Daten folgen dem Prozess: ausstehend, angenommen, abgelehnt. E-Mail bleibt privat oder als Hash gespeichert. Indizes folgen echten Abfragen von Website und Adminbereich. Die Migration wird vor Veröffentlichung beschrieben und getestet.
C#-Modell: ein sinnvolles Minimum
Eine Entität sollte ausdrücken, was dauerhaft wahr ist. Das Aussehen des Formulars muss sie nicht kennen. Wohl aber Regeln, die auch nach einem UI-Wechsel gelten.
public sealed class CustomerReview
{
public Guid Id { get; set; }
public string DisplayName { get; set; } = null!;
public string? CompanyName { get; set; }
public string EmailHash { get; set; } = null!;
public int Rating { get; set; }
public string Body { get; set; } = null!;
public ReviewStatus Status { get; set; } = ReviewStatus.Pending;
public DateTimeOffset CreatedAt { get; set; } = DateTimeOffset.UtcNow;
public DateTimeOffset? PublishedAt { get; set; }
}
public enum ReviewStatus
{
Pending = 0,
Approved = 1,
Rejected = 2
}
Entscheidend: Dieses Modell veröffentlicht keine private E-Mail-Adresse. Wird sie zur Verifizierung gebraucht, bleibt sie außerhalb des öffentlichen Modells oder wird für Deduplizierung als Hash gespeichert. Öffentlich zählen Bewertung, sichtbarer Autor, Unternehmen und Punktzahl. Für die Verwaltung zählen Quelle, Historie und Entscheidung.
Warum ist ein Status besser als nur Published = true?
Weil der echte Prozess mehr als zwei Zustände hat. Eine Bewertung kann warten, angenommen, abgelehnt, zur Korrektur zurückgegeben oder später verborgen werden. Ein Boolean reicht bis zur ersten Ausnahme. Statuswerte ermöglichen Entwicklung ohne chaotische Zusatzflags.
EF-Core-Konfiguration: Limits, Indizes, Absicht
Wirkt EF-Konfiguration wie eine Formalität, wurde oft die Frage vergessen: Wie wird diese Tabelle in Produktion gelesen?
builder.Entity<CustomerReview>(e =>
{
e.Property(x => x.DisplayName)
.HasMaxLength(120)
.IsRequired();
e.Property(x => x.CompanyName)
.HasMaxLength(160);
e.Property(x => x.EmailHash)
.HasMaxLength(96)
.IsRequired();
e.Property(x => x.Body)
.HasMaxLength(1800)
.IsRequired();
e.Property(x => x.Rating)
.IsRequired();
e.HasIndex(x => new { x.Status, x.PublishedAt });
e.HasIndex(x => x.EmailHash);
});
Der Index Status + PublishedAt ist sinnvoll, weil die öffentliche Seite nur angenommene Bewertungen lädt, meist nach Veröffentlichungsdatum sortiert. Ein Index auf EmailHash hilft bei Moderation und Missbrauchsbegrenzung. Das sind keine Vorratsindizes, sondern Indizes für bekannte Systempfade.
Migration: ein lesbares Skript
Eine Migration darf keine Blackbox sein. Liest das Team das erzeugte Skript nicht, weiß es nicht wirklich, was in der Datenbank ankommt.
migrationBuilder.CreateTable(
name: "CustomerReviews",
columns: table => new
{
Id = table.Column<Guid>(nullable: false),
DisplayName = table.Column<string>(maxLength: 120, nullable: false),
CompanyName = table.Column<string>(maxLength: 160, nullable: true),
EmailHash = table.Column<string>(maxLength: 96, nullable: false),
Rating = table.Column<int>(nullable: false),
Body = table.Column<string>(maxLength: 1800, nullable: false),
Status = table.Column<int>(nullable: false),
CreatedAt = table.Column<DateTimeOffset>(nullable: false),
PublishedAt = table.Column<DateTimeOffset>(nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_CustomerReviews", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_CustomerReviews_Status_PublishedAt",
table: "CustomerReviews",
columns: new[] { "Status", "PublishedAt" });
Eine neue Tabelle ist vergleichsweise sicher. Schwieriger sind Änderungen bestehender Tabellen: eine Spalte NOT NULLhinzufügen, einen Index umbauen, Typen ändern, Daten nachfüllen oder ein altes Feld löschen. Dann müssen Sperren und Laufzeit geplant werden.
Was prüfe ich vor der Migration einer bestehenden Tabelle?
- Hat die Spalte einen Standardwert, und erzwingt sie keine vollständige Sperre einer großen Tabelle?
- Kann der Backfill in Chargen laufen?
- Ist neuer Code mit alter und neuer Struktur kompatibel?
- Bedeutet Rollback Code, Daten oder beides zurückzusetzen?
- Haben wir ein Backup und einen Wiederherstellungstest statt nur eines Backup-Versprechens?
Azure: Migration als ausdrücklicher Schritt statt Startmagie
Automatische Migrationen beim Anwendungsstart sind bequem, können in Produktion aber riskant sein. Eine schwere Migration verzögert den Start, verursacht Timeouts oder blockiert mehrere Instanzen. Für einfache Änderungen ist das kontrollierbar; bei kritischen Datenbanken ist ein ausdrücklicher Schritt oft besser.
dotnet ef migrations script --idempotent -o ./artifacts/db.sql
az webapp deployment slot swap \
--name arccloud-api \
--resource-group prod \
--slot staging
psql "$DATABASE_URL" \
-v ON_ERROR_STOP=1 \
-f ./artifacts/db.sql
curl -fsS https://www.arccloudstudio.com/health
Es geht nicht um ein einziges vorgeschriebenes Verfahren. Entscheidend ist: Die Migration muss sichtbar, wiederholbar und überprüfbar sein. Bei Fehlern muss klar werden, ob Schema, Daten, Anwendung, Routing oder Konfiguration betroffen sind.
Runbook für das Datenbank-Deployment
- Preflight: Backup, Verbindung und Anwendungsversion prüfen.
- Script: idempotentes SQL erzeugen und vor dem Ausführen lesen.
- Apply: Migration separat ausführen und bei Fehler abbrechen.
- Smoke: Endpoint, Speichern, Lesen und Adminbereich prüfen.
- Closeout: Nachweis speichern: Log, Zeit, Ergebnis und Datenbankänderungen.
Kleine Debugkonsole: Was passiert nach dem Deployment wirklich?
Nach dem Deployment frage ich nicht „Läuft es?“, sondern „Was belegt, dass es läuft?“ Der Unterschied ist groß. Hier ein Beispiel einer minimalen Abnahme nach einer Modelländerung.
SELECT "Status", count(*)
FROM "CustomerReviews"
GROUP BY "Status";
SELECT indexname
FROM pg_indexes
WHERE tablename = 'CustomerReviews';
SELECT "DisplayName", "Rating", "PublishedAt"
FROM "CustomerReviews"
WHERE "Status" = 1
ORDER BY "PublishedAt" DESC
LIMIT 5;
SQL Statuszahlen: pending=3, approved=0, rejected=0
IDX IX_CustomerReviews_Status_PublishedAt gefunden
API Öffentlicher Endpoint liefert nur angenommene Bewertungen
ADMIN Moderationsaktion erzeugt einen Auditnachweis
Häufigster Fehler: Das Modell wächst schneller als die Verantwortung
Schlechtes Code First lässt das C#-Modell schneller wachsen als die Verantwortung für Daten. Nach sechs Monaten hat die Datenbank unklare Spalten, grundlose Indizes, undokumentierte Migrationen und Datensätze, die niemand sicher korrigieren kann.
Gutes Code First macht es umgekehrt. Code liefert Tempo, die Datenbank bleibt ein Produktionsvertrag. Jede Änderung beantwortet: Wer schreibt und liest das, wie filtern wir, was ist privat, wie gehen wir zurück und wie prüfen wir nach dem Deployment?
Checkliste vor einer Migration
- Modell: Die Entität beschreibt den Prozess, nicht nur Formularfelder.
- Limits: Texte, Namen und Statuswerte haben realistische Grenzen.
- Indizes: Sie folgen den Abfragen von öffentlicher Website und Adminbereich.
- Datenschutz: E-Mail und sensible Daten gelangen nicht in die öffentliche API.
- Deployment: Die Migration ist ausdrücklich, protokolliert und wiederholbar.
- Abnahme: Nach dem Deployment gibt es Nachweise statt nur „ist durch“.
Mini-Test für dein Projekt
Nimm eine Tabelle deiner Anwendung und beantworte ohne Blick in den Code:
Sind die Antworten nicht offensichtlich, ist das Projekt nicht automatisch schlecht. Die Datenbank verlangt nur nach Ordnung, bevor daraus ein teures Problem wird.
Steht eine Datenbankänderung vor Produktion an?
Prüfen wir Modell, Migration und Risiken, bevor Nutzer den Fehler spüren.
Beschreibe Modul, Tabelle oder Problem. Ich melde mich mit einer konkreten Reihenfolge: was prüfen, was korrigieren und wie das Deployment ohne Rätselraten abnehmen.