Liefern Sie sicherere, KI-generierte Apps, indem Sie PostgreSQL-Constraints für NOT NULL, CHECK, UNIQUE und FOREIGN KEY verwenden — bevor Sie sich nur auf Code und Tests verlassen.

KI-generierter Code wirkt oft korrekt, weil er den Happy Path abdeckt. In echten Anwendungen treten Fehler im unordentlichen Mittelfeld auf: Ein Formular sendet einen leeren String statt NULL, ein Hintergrundjob wird erneut ausgeführt und legt denselben Datensatz zweimal an, oder ein Delete entfernt eine Elternzeile und lässt Kinder zurück. Das sind keine exotischen Bugs. Sie zeigen sich als leere Pflichtfelder, doppelte "einzigartige" Werte und verwaiste Zeilen, die ins Nichts verweisen.
Solche Probleme schlüpfen auch durch Code-Reviews und einfache Tests, aus einem einfachen Grund: Reviewer lesen die Absicht, nicht jede Randbedingung. Tests decken meist einige typische Fälle ab, nicht Wochen echten Nutzerverhaltens, CSV-Imports, unzuverlässige Netzwerk-Retries oder gleichzeitige Anfragen. Wenn ein Assistent den Code erzeugt hat, kann er kleine, aber kritische Prüfungen übersehen, wie Whitespace trimmen, Bereichsvalidierung oder Schutz gegen Race Conditions.
„Constraints first, code second“ bedeutet: Legen Sie unverhandelbare Regeln in die Datenbank, damit schlechte Daten gar nicht erst gespeichert werden, egal welcher Codepfad schreibt. Ihre App sollte Eingaben weiterhin validieren, um bessere Fehlermeldungen zu liefern, aber die Datenbank setzt die Wahrheit durch. Genau hier glänzen PostgreSQL-Constraints: Sie schützen vor ganzen Kategorien von Fehlern.
Ein kurzes Beispiel: Stellen Sie sich ein kleines CRM vor. Ein KI-generiertes Importskript legt Kontakte an. In einer Zeile steht die E-Mail als "" (leer), zwei Zeilen wiederholen dieselbe E-Mail in unterschiedlicher Groß-/Kleinschreibung, und ein Kontakt referenziert ein account_id, das nicht existiert, weil das Account in einem anderen Prozess gelöscht wurde. Ohne Constraints kann all das in Produktion landen und später Reports kaputtmachen.
Mit den richtigen Regeln in der Datenbank schlagen diese Schreibvorgänge sofort fehl, nahe an der Quelle. Pflichtfelder können nicht fehlen, Duplikate können sich bei Retries nicht einschleichen, Beziehungen können nicht auf gelöschte oder nicht existente Datensätze zeigen, und Werte können nicht außerhalb erlaubter Bereiche liegen.
Constraints verhindern nicht jeden Bug. Sie beheben weder eine verwirrende UI, eine falsche Rabattberechnung noch eine langsame Abfrage. Aber sie stoppen, dass sich schlechte Daten stillschweigend ansammeln — oft der Punkt, an dem „KI-generierte Edge-Case-Bugs“ teuer werden.
Ihre App ist selten nur eine Codebasis, die mit einem Nutzer spricht. Ein typisches Produkt hat eine Web-UI, eine Mobile-App, Admin-Oberflächen, Hintergrundjobs, CSV-Imports und manchmal Drittanbieter-Integrationen. Jeder Pfad kann Daten erzeugen oder ändern. Wenn jeder Pfad sich dieselben Regeln merken muss, vergisst einer davon.
Die Datenbank ist der gemeinsame Ort, den alle nutzen. Wenn Sie sie als letzte Instanz behandeln, gelten die Regeln automatisch für alles. PostgreSQL-Constraints verwandeln „wir gehen davon aus, dass das immer wahr ist“ in „das muss wahr sein, sonst schlägt der Schreibvorgang fehl."
Gerade bei KI-generiertem Code wird das noch wichtiger. Ein Modell könnte Formularvalidierung in einer React-UI hinzufügen, aber eine Ecke in einem Hintergrundjob übersehen. Oder es geht mit Happy-Path-Daten gut um, bricht aber, wenn ein echter Kunde etwas Unerwartetes eingibt. Constraints fangen Probleme genau beim Versuch, schlechte Daten zu speichern, ab — nicht Wochen später, wenn Sie seltsame Reports debuggen.
Wenn Sie Constraints überspringen, sind schlechte Daten oft still. Das Speichern klappt, die App macht weiter, und das Problem taucht später als Support-Ticket, Abrechnungsfehler oder als ein Dashboard auf, dem niemand vertraut. Aufräumen ist teuer, weil Sie Geschichte reparieren, nicht eine einzelne Anfrage.
Schlechte Daten schleichen sich meist durch alltägliche Situationen ein: Eine neue Client-Version sendet ein Feld als leer statt gar nicht vorhanden, ein Retry erzeugt Duplikate, eine Admin-Änderung umgeht UI-Prüfungen, eine Importdatei hat inkonsistente Formatierung oder zwei Nutzer aktualisieren verwandte Datensätze gleichzeitig.
Ein nützliches Denkmodell: Akzeptieren Sie Daten nur, wenn sie an der Grenze gültig sind. Praktisch sollte diese Grenze die Datenbank einschließen, weil die Datenbank alle Schreibvorgänge sieht.
NOT NULL ist die einfachste PostgreSQL-Constraint und verhindert erstaunlich viele Fehlerklassen. Wenn ein Wert für die Zeile sinnvoll vorhanden sein muss, lassen Sie die Datenbank das erzwingen.
NOT NULL ist meist richtig für Identifikatoren, erforderliche Namen und Zeitstempel. Wenn Sie ohne einen Wert keinen gültigen Datensatz erstellen können, erlauben Sie ihn nicht leer. In einem kleinen CRM ist ein Lead ohne Owner oder ohne Erstellungszeit kein „teilweiser Lead“. Das sind kaputte Daten, die später seltsames Verhalten verursachen.
NULL schleicht sich bei KI-generiertem Code häufiger ein, weil es leicht ist, „optionale“ Pfade zu erzeugen, ohne es zu bemerken. Ein Formularfeld kann in der UI optional sein, eine API akzeptiert vielleicht einen fehlenden Key, und ein Branch einer Create-Funktion überspringt das Setzen eines Werts. Alles kompiliert, der Happy-Path-Test besteht. Dann importieren echte Nutzer eine CSV mit leeren Zellen oder ein Mobile-Client sendet andere Payloads, und plötzlich landet NULL in der Datenbank.
Ein gutes Muster ist, NOT NULL mit sinnvollen Defaults für Felder zu kombinieren, die das System selbst besitzt:
created_at TIMESTAMP NOT NULL DEFAULT now()status TEXT NOT NULL DEFAULT 'new'is_active BOOLEAN NOT NULL DEFAULT trueDefaults sind nicht immer die beste Wahl. Setzen Sie nicht einfach Default-Werte für nutzergelieferte Felder wie email oder company_name, nur um NOT NULL zufriedenzustellen. Ein leerer String ist nicht „gültiger“ als NULL — er verbirgt nur das Problem.
Wenn Sie unsicher sind, entscheiden Sie, ob der Wert wirklich unbekannt ist oder ob er einen anderen Zustand repräsentiert. Wenn „noch nicht angegeben“ aussagekräftig ist, überlegen Sie eine separate Status-Spalte statt überall NULL zuzulassen. Zum Beispiel phone nullable lassen, aber phone_status mit Werten wie missing, requested oder verified hinzufügen. So bleibt die Bedeutung im ganzen Code konsistent.
Eine CHECK-Constraint ist ein Versprechen Ihrer Tabelle: Jede Zeile muss eine Regel erfüllen, jedes Mal. Sie ist eine der einfachsten Möglichkeiten, Randfälle zu verhindern, die im Code zwar plausibel aussehen, in der Realität aber keinen Sinn ergeben.
CHECK-Constraints eignen sich am besten für Regeln, die nur von Werten derselben Zeile abhängen: numerische Bereiche, erlaubte Werte und einfache Beziehungen zwischen Spalten.
-- 1) Totals should never be negative
ALTER TABLE invoices
ADD CONSTRAINT invoices_total_nonnegative
CHECK (total_cents >= 0);
-- 2) Enum-like allowed values without adding a custom type
ALTER TABLE tickets
ADD CONSTRAINT tickets_status_allowed
CHECK (status IN ('new', 'open', 'waiting', 'closed'));
-- 3) Date order rules
ALTER TABLE subscriptions
ADD CONSTRAINT subscriptions_date_order
CHECK (end_date IS NULL OR end_date >= start_date);
Eine gute CHECK ist auf einen Blick lesbar. Behandeln Sie sie wie Dokumentation für Ihre Daten. Bevorzugen Sie kurze Ausdrücke, klare Constraint-Namen und vorhersehbare Muster.
CHECK ist nicht das richtige Werkzeug für alles. Wenn eine Regel andere Zeilen nachschlagen, Aggregationen braucht oder Tabellen vergleicht (z. B. „ein Account darf sein Plan-Limit nicht überschreiten“), bleibt die Logik in der Applikation, in Triggern oder in einem kontrollierten Hintergrundjob.
Eine UNIQUE-Regel ist simpel: Die Datenbank weigert sich, zwei Zeilen zu speichern, die denselben Wert in der eingeschränkten Spalte (oder dieselbe Kombination über mehrere Spalten) haben. Das beseitigt eine ganze Klasse von Fehlern, bei denen ein Create-Pfad zweimal läuft, ein Retry passiert oder zwei Nutzer gleichzeitig dasselbe absenden.
UNIQUE garantiert keine Anwesenheit (NOT NULL), kein Format (CHECK) und auch nicht Ihre Vorstellung von Gleichheit (Groß-/Kleinschreibung, Leerzeichen, Interpunktion), sofern Sie das nicht definieren.
Gängige Stellen, an denen Sie üblicherweise Einzigartigkeit wollen, sind die E-Mail in einer Users-Tabelle, external_id aus einem anderen System oder ein Name, der innerhalb eines Accounts einzigartig sein muss, z. B. (account_id, name).
Ein Stolperstein: NULL und UNIQUE. In PostgreSQL wird NULL als „unbekannt“ behandelt, sodass mehrere NULL-Werte unter einer UNIQUE-Constraint erlaubt sind. Wenn Sie meinen „der Wert muss vorhanden und einzigartig sein“, kombinieren Sie UNIQUE mit NOT NULL.
Ein praktisches Muster für nutzerorientierte Identifikatoren ist fallunabhängige (case-insensitive) Einzigartigkeit. Menschen tippen „[email protected]“ und später „[email protected]“ und erwarten, dass das dieselbe Mail ist.
-- Case-insensitive unique email
CREATE UNIQUE INDEX users_email_unique_ci
ON users (lower(email));
-- Unique contact name per account
ALTER TABLE contacts
ADD CONSTRAINT contacts_account_name_unique UNIQUE (account_id, name);
Definieren Sie, was „Duplikat“ für Ihre Nutzer bedeutet (Groß-/Kleinschreibung, Leerzeichen, pro-Account vs global), und kodieren Sie es einmal, damit jeder Codepfad derselben Regel folgt.
Ein FOREIGN KEY sagt: „Diese Zeile muss auf eine echte Zeile dort drüben zeigen.“ Ohne ihn kann Code stillschweigend verwaiste Datensätze erzeugen, die isoliert gültig erscheinen, die App aber später kaputtmachen. Zum Beispiel: eine Notiz, die auf einen Kunden zeigt, der gelöscht wurde, oder eine Rechnung, die auf eine User-ID zeigt, die es nie gab.
Foreign Keys sind besonders wichtig, wenn zwei Aktionen nahe beieinander passieren: ein Delete und ein Create, ein Retry nach Timeout oder ein Hintergrundjob mit veralteten Daten. Die Datenbank ist besser darin, Konsistenz durchzusetzen, als dass sich jeder App-Pfad das prüfen muss.
Die ON DELETE-Option sollte der realen Bedeutung der Beziehung entsprechen. Fragen Sie: „Wenn das Parent verschwindet, darf das Child weiterexistieren?"
RESTRICT (oder NO ACTION): verhindert das Löschen des Parents, wenn Kinder existieren.CASCADE: löscht beim Löschen des Parents auch die Kinder.SET NULL: behält das Kind, entfernt aber die Verknüpfung.Seien Sie vorsichtig mit CASCADE. Es kann korrekt sein, aber auch mehr löschen als erwartet, wenn ein Bug oder eine Admin-Aktion das Parent entfernt.
In Multi-Tenant-Apps geht es bei Foreign Keys nicht nur um Korrektheit. Sie verhindern auch Datenleckage zwischen Accounts. Ein gängiges Muster ist, account_id in jeder tenant-eigenen Tabelle zu haben und Beziehungen darüber zu verknüpfen.
CREATE TABLE contacts (
account_id bigint NOT NULL,
id bigint GENERATED ALWAYS AS IDENTITY,
PRIMARY KEY (account_id, id)
);
CREATE TABLE notes (
account_id bigint NOT NULL,
id bigint GENERATED ALWAYS AS IDENTITY,
contact_id bigint NOT NULL,
body text NOT NULL,
PRIMARY KEY (account_id, id),
FOREIGN KEY (account_id, contact_id)
REFERENCES contacts (account_id, id)
ON DELETE RESTRICT
);
Das erzwingt im Schema „wer gehört zu wem“: Eine Notiz kann nicht auf einen Kontakt in einem anderen Account zeigen, selbst wenn der App-Code (oder eine LLM-generierte Query) es versucht.
Beginnen Sie mit einer kurzen Liste von Invarianten: Fakten, die immer wahr sein müssen. Halten Sie sie schlicht. „Jeder Kontakt braucht eine E-Mail.“ „Ein Status muss einer von wenigen erlaubten Werten sein.“ „Eine Rechnung muss zu einem echten Kunden gehören.“ Das sind die Regeln, die die Datenbank jedes Mal durchsetzen soll.
Rollen Sie Änderungen in kleinen Migrationen aus, damit die Produktion nicht überrascht wird:
NOT NULL, UNIQUE, CHECK, FOREIGN KEY).Das Unordentliche sind die bereits existierenden schlechten Daten. Planen Sie dafür. Bei Duplikaten wählen Sie eine Gewinner-Zeile, mergen die restlichen und behalten eine kleine Audit-Notiz. Bei fehlenden Pflichtfeldern wählen Sie nur dann einen sicheren Default, wenn er wirklich sicher ist; ansonsten isolieren. Bei kaputten Beziehungen weisen Sie Child-Zeilen dem korrekten Parent zu oder löschen die fehlerhaften Zeilen.
Nach jeder Migration validieren Sie mit ein paar Schreibvorgängen, die fehlschlagen sollten: Fügen Sie eine Zeile mit fehlendem Pflichtwert ein, insertieren Sie einen doppelten Schlüssel, fügen Sie einen außerhalb liegenden Wert ein und referenzieren Sie eine fehlende Parent-Zeile. Fehlgeschlagene Schreibvorgänge sind nützliche Signale — sie zeigen, wo die App bisher stillschweigend auf „Best Effort“ gebaut hat.
Stellen Sie sich ein kleines CRM vor: Accounts (jeder Kunde Ihres SaaS), Firmen, Kontakte bei diesen Firmen und Deals, die an eine Firma gebunden sind.
Das ist genau die Art App, die Leute schnell mit einem Chat-Tool erzeugen. Im Demo-Modus sieht alles gut aus, aber echte Daten werden schnell unordentlich. Zwei Bugs tauchen meist früh auf: doppelte Kontakte (dasselbe E-Mail wird leicht unterschiedlich eingegeben) und Deals ohne Firma, weil ein Pfad vergaß, company_id zu setzen. Ein weiterer Klassiker ist ein negativer Deal-Wert nach einem Refactor oder Parsing-Fehler.
Die Lösung sind nicht mehr If-Statements. Es sind einige wohlgewählte Constraints, die es unmöglich machen, schlechte Daten zu speichern.
-- Contacts: prevent duplicates per account
ALTER TABLE contacts
ADD CONSTRAINT contacts_account_email_uniq UNIQUE (account_id, email);
-- Deals: require a company and keep the relationship valid
ALTER TABLE deals
ALTER COLUMN company_id SET NOT NULL,
ADD CONSTRAINT deals_company_fk
FOREIGN KEY (company_id) REFERENCES companies(id);
-- Deals: deal value cannot be negative
ALTER TABLE deals
ADD CONSTRAINT deals_value_nonneg CHECK (deal_value >= 0);
-- A few obvious required fields
ALTER TABLE companies
ALTER COLUMN name SET NOT NULL;
ALTER TABLE contacts
ALTER COLUMN email SET NOT NULL;
Dabei geht es nicht um Strenge um ihrer selbst willen. Sie verwandeln vage Erwartungen in Regeln, die die Datenbank jedes Mal durchsetzt, egal welcher Teil der App schreibt.
Sobald diese Constraints stehen, wird die App einfacher. Sie können viele defensive Prüfungen entfernen, die versuchen, Duplikate nachträglich zu erkennen. Fehlschläge werden klar und handhabbar (z. B. „E-Mail existiert bereits für dieses Account“ statt merkwürdiges nachgelagertes Verhalten). Und wenn eine generierte API-Route ein Feld vergisst oder einen Wert falsch behandelt, schlägt das Schreiben sofort fehl, statt die DB stillschweigend zu korrumpieren.
Constraints funktionieren am besten, wenn sie zur tatsächlichen Geschäftspraxis passen. Die meisten Probleme entstehen, wenn Regeln angelegt werden, die sich im Moment „sicher“ anfühlen, später aber überraschend sind.
Eine häufige Fallgrube ist ON DELETE CASCADE überall zu verwenden. Es sieht ordentlich aus, bis jemand ein Parent löscht und die DB die Hälfte des Systems entfernt. Cascades können für wirklich zugehörige Daten (z. B. Draft-Line-Items, die niemals alleine existieren sollten) korrekt sein, sind aber riskant für wichtige Datensätze (Kunden, Rechnungen, Tickets). Wenn Sie unsicher sind, bevorzugen Sie RESTRICT und behandeln Deletes bewusst.
Ein weiteres Problem sind zu enge CHECK-Regeln. „Status muss 'new', 'won' oder 'lost' sein“ klingt okay, bis Sie „paused“ oder „archived“ brauchen. Eine gute CHECK beschreibt eine stabile Wahrheit, nicht eine vorübergehende UI-Option. amount >= 0 altert gut. country IN (...) oft nicht.
Wiederkehrende Probleme, wenn Teams Constraints nachträglich zu einem bereits laufenden, generierten Code hinzufügen:
CASCADE als Aufräumwerkzeug benutzen und dann mehr löschen als beabsichtigt.CHECK-Regeln so eng machen, dass sie zukünftig gültige Fälle blockieren.Zur Performance: PostgreSQL legt automatisch einen Index für UNIQUE an, aber Foreign Keys indexieren die referenzierende Spalte nicht automatisch. Ohne diesen Index können Updates und Deletes am Parent langsam werden, weil Postgres die Child-Tabelle scannen muss, um Referenzen zu prüfen.
Bevor Sie eine Regel verschärfen, finden Sie existierende Zeilen, die daran scheitern würden, entscheiden Sie, ob Sie sie reparieren oder isolieren, und rollen Sie die Änderung schrittweise aus.
Bevor Sie deployen, nehmen Sie sich fünf Minuten pro Tabelle und schreiben Sie auf, was immer wahr sein muss. Wenn Sie es auf Englisch sagen können, lässt es sich meist mit einer Constraint durchsetzen.
Stellen Sie für jede Tabelle diese Fragen:
Wenn Sie ein Chat-getriebenes Build-Tool verwenden, behandeln Sie diese Invarianten als Akzeptanzkriterien für die Daten, nicht als optionale Notizen. Zum Beispiel: „Ein Deal-Betrag muss nicht-negativ sein“, „Eine Kontakt-E-Mail ist pro Workspace einzigartig“, „Eine Aufgabe muss auf einen echten Kontakt verweisen.“ Je expliziter die Regeln, desto weniger Raum für versehentliche Edge Cases.
Koder.ai (koder.ai) includes features like planning mode, snapshots and rollback, and source code export, which can make it easier to iterate on schema changes safely while you tighten constraints over time.
Ein einfaches Rollout-Muster, das in echten Teams funktioniert: Wählen Sie eine Tabelle mit hohem Nutzen (Users, Orders, Invoices, Contacts), fügen Sie 1–2 Constraints hinzu, die die schlimmsten Fehler verhindern (oft NOT NULL und UNIQUE), beheben Sie die Schreibvorgänge, die fehlschlagen, und wiederholen Sie den Vorgang. Regeln schrittweise zu verschärfen ist besser als eine große, riskante Migration.