Leverera säkrare AI-genererade appar genom att förlita dig på PostgreSQL-constraints för NOT NULL, CHECK, UNIQUE och FOREIGN KEY innan kod och tester.

AI-genererad kod ser ofta korrekt ut eftersom den hanterar happy-path. Riktiga applikationer faller på mitten där det är rörigt: ett formulär skickar en tom sträng istället för null, ett bakgrundsjobb försöker igen och skapar samma rad två gånger, eller en borttagning tar bort en förälder och lämnar barnen utan referens. Det här är inte exotiska buggar. De visar sig som tomma obligatoriska fält, dubbla "unika" värden och föräldralösa rader som pekar mot ingenting.
De smiter även igenom kodgranskning och grundläggande tester av en enkel anledning: granskare läser avsikten, inte varje kantfall. Tester täcker vanligtvis ett par typiska exempel, inte veckor av verkligt användarbeteende, import från CSV, fladdriga nätverksförsök eller samtidiga förfrågningar. Om en assistent genererat koden kan den missa små men kritiska kontroller som att trimma mellanslag, validera intervall eller skydda mot race conditions.
"Constraints first, code second" betyder att du lägger icke-förhandlingsbara regler i databasen så att dåliga data inte kan sparas, oavsett vilken kodväg som försöker skriva dem. Din app bör fortfarande validera indata för bättre felmeddelanden, men databasen upprätthåller sanningen. Det är där PostgreSQL-constraints briljerar: de skyddar dig från hela kategorier av misstag.
Ett snabbt exempel: tänk dig ett litet CRM. Ett AI-genererat importskript skapar kontakter. En rad har en email som "" (tom), två rader upprepar samma e-post med olika versalisering, och en kontakt refererar ett account_id som inte finns eftersom kontot raderades i en annan process. Utan constraints kan allt detta hamna i produktion och senare förstöra rapporter.
Med rätt databassregler misslyckas de skrivningarna omedelbart, nära källan. Obligatoriska fält kan inte saknas, dubbletter kan inte smyga in vid retries, relationer kan inte peka på borttagna eller icke-existerande rader, och värden kan inte hamna utanför tillåtna intervall.
Constraints stoppar inte alla buggar. De fixar inte en förvirrande UI, en felaktig rabattberäkning eller en långsam fråga. Men de hindrar dåliga data från att tyst samlas, vilket ofta är där "AI-genererade kantfallsbuggar" blir dyra.
Din app är sällan en kodbas som bara pratar med en användare. En typisk produkt har en webb-UI, en mobilapp, adminskärmar, bakgrundsjobb, import från CSV och ibland tredjepartsintegrationer. Varje väg kan skapa eller ändra data. Om varje väg måste komma ihåg samma regler kommer en att glömma.
Databasen är den plats de alla delar. När du behandlar den som den slutgiltiga grindvakt så tillämpas reglerna automatiskt på allt. PostgreSQL-constraints förvandlar "vi antar att detta alltid är sant" till "detta måste vara sant, annars misslyckas skrivningen."
AI-genererad kod gör detta ännu viktigare. En modell kan lägga till formulärvalidering i en React-UI men missa ett hörnfall i ett bakgrundsjobb. Eller den kan hantera happy-path-data bra men gå sönder när en verklig kund matar in något oväntat. Constraints fångar problem i det ögonblick då dåliga data försöker komma in, inte veckor senare när du felsöker konstiga rapporter.
När du hoppar över constraints är dåliga data ofta tysta. Sparandet lyckas, appen går vidare och problemet visar sig senare som ett supportärende, en fakturaskillnad eller en dashboard ingen litar på. Rensning är dyrt eftersom du fixar historik, inte en enskild förfrågan.
Dåliga data smyger vanligtvis in genom vardagliga situationer: en ny klientapp-version skickar ett fält som tomt istället för saknat, en retry skapar dubbletter, en adminändring kringgår UI-kontroller, en importfil har inkonsekvent formatering eller två användare uppdaterar relaterade rader samtidigt.
En användbar mental modell: acceptera data bara om den är giltig vid gränsen. I praktiken bör den gränsen inkludera databasen, eftersom databasen ser alla skrivningar.
NOT NULL är den enklaste PostgreSQL-constrainten och förhindrar en överraskande stor klass av buggar. Om ett värde måste finnas för att raden ska vara meningsfull, låt databasen upprätthålla det.
NOT NULL är oftast rätt för identifierare, obligatoriska namn och tidsstämplar. Om du inte kan skapa en giltig rad utan det, tillåt inte att det är tomt. I ett litet CRM är en lead utan ägare eller skapad-tid inte en "delvis lead". Det är trasig data som kommer orsaka konstigt beteende senare.
NULL smyger in oftare med AI-genererad kod eftersom det är lätt att skapa "valfria" vägar utan att märka det. Ett formulärfält kan vara frivilligt i UI, ett API kan acceptera en saknad nyckel och en gren i en create-funktion kan hoppa över att sätta ett värde. Allt kompilerar och happy-path-testet går igenom. Sedan importerar riktiga användare en CSV med tomma celler, eller en mobilklient skickar en annan payload, och NULL hamnar i databasen.
Ett bra mönster är att kombinera NOT NULL med ett rimligt default för fält som systemet äger:
created_at TIMESTAMP NOT NULL DEFAULT now()status TEXT NOT NULL DEFAULT 'new'is_active BOOLEAN NOT NULL DEFAULT trueDefaults är inte alltid en fördel. Sätt inte default för användargivna fält som email eller company_name bara för att tillfredsställa NOT NULL. En tom sträng är inte "mer giltig" än NULL. Den döljer bara problemet.
När du är osäker, avgör om värdet verkligen är okänt, eller om det representerar ett annat tillstånd. Om "inte angivet än" är meningsfullt, överväg en separat statuskolumn istället för att tillåta NULL överallt. Till exempel: låt phone vara nullable, men lägg till phone_status med värden som missing, requested eller verified. Det håller betydelsen konsekvent i koden.
En CHECK-constraint är ett löfte din tabell ger: varje rad måste uppfylla en regel, varje gång. Det är ett av de enklaste sätten att förhindra att kantfall tyst skapar rader som ser fina ut i koden men inte är meningsfulla i verkligheten.
CHECK-constraints fungerar bäst för regler som bara beror på värden i samma rad: numeriska intervall, tillåtna värden och enkla relationer mellan kolumner.
-- 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);
En bra CHECK är lättläst vid en snabb blick. Behandla den som dokumentation för din data. Föredra korta uttryck, tydliga constraint-namn och förutsägbara mönster.
CHECK är inte rätt verktyg för allt. Om en regel behöver slå upp andra rader, aggregera data eller jämföra över tabeller (till exempel "ett konto får inte överskrida sin planbegränsning"), behåll den logiken i applikationskod, triggers eller ett kontrollerat bakgrundsjobb.
En UNIQUE-regel är enkel: databasen vägrar lagra två rader som har samma värde i den begränsade kolumnen (eller samma kombination av värden över flera kolumner). Det här tar bort en hel klass av buggar där en "create"-väg körs två gånger, en retry sker eller två användare skickar samma sak samtidigt.
UNIQUE garanterar inga dubbletter för de exakta värden du definierar. Det garanterar inte att värdet finns (NOT NULL), att det följer ett format (CHECK) eller att det matchar din uppfattning om lika värde (skiftläge, mellanslag, skiljetecken) om du inte definierar det.
Vanliga ställen där du oftast vill ha unikhet är email på en user-tabell, external_id från ett annat system eller ett namn som måste vara unikt inom ett konto som (account_id, name).
En fälla: NULL och UNIQUE. I PostgreSQL behandlas NULL som "okänt", så flera NULL-värden tillåts under en UNIQUE-constraint. Om du menar "värdet måste finnas och vara unikt", kombinera UNIQUE med NOT NULL.
Ett praktiskt mönster för användarorienterade identifierare är skiftlagsoberoende unikhet. Människor skriver "[email protected]" och senare "[email protected]" och förväntar sig att det är samma.
-- 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);
Definiera vad "dubblett" betyder för dina användare (skiftläge, mellanslag, per-konto vs globalt), och koda det en gång så att alla kodvägar följer samma regel.
En FOREIGN KEY säger: "denna rad måste peka på en riktig rad där borta." Utan den kan koden tyst skapa föräldralösa poster som ser giltiga ut isolerat men bryter appen senare. Till exempel: en not som refererar en kund som raderats, eller en faktura som pekar på ett user-id som aldrig funnits.
Foreign keys spelar störst roll när två åtgärder händer nära varandra: en borttagning och en skapelse, en retry efter timeout eller ett bakgrundsjobb som körs med föråldrade data. Databasen är bättre på att upprätthålla konsistens än att förlita sig på att varje app-väg kommer ihåg att kontrollera.
ON DELETE-alternativet bör matcha den verkliga betydelsen av relationen. Fråga: "Om föräldern försvinner, ska barnet fortfarande finnas?"
RESTRICT (eller NO ACTION): blockera borttagning av förälder om barn finns.CASCADE: att ta bort föräldern tar också bort barnen.SET NULL: behåll barnet men ta bort länken.Var försiktig med CASCADE. Det kan vara korrekt, men det kan också radera mer än du förväntat när en bugg eller adminåtgärd tar bort en förälder.
I multi-tenant-appar handlar foreign keys inte bara om korrekthet. De förhindrar också cross-account-läckage. Ett vanligt mönster är att inkludera account_id på varje tenant-ägd tabell och binda relationer genom det.
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
);
Detta upprätthåller "vem äger vad" i schemat: en note kan inte peka på en contact i ett annat konto, även om appkoden (eller en LLM-genererad fråga) försöker.
Börja med att skriva en kort lista över invariants: fakta som alltid måste vara sanna. Håll dem enkla. "Varje kontakt behöver en e-post." "Ett statusvärde måste vara ett av några tillåtna värden." "En faktura måste tillhöra en riktig kund." Det här är reglerna du vill att databasen ska upprätthålla varje gång.
Rulla ut ändringar i små migrationer så att produktionen inte blir överraskad:
NOT NULL, UNIQUE, CHECK, FOREIGN KEY).Det röriga är befintliga dåliga data. Planera för det. För dubbletter, välj en vinnarrad, slå ihop resten och behåll en liten revisionsanteckning. För saknade obligatoriska fält, välj ett säkert default bara om det verkligen är säkert; annars karantänera. För brutna relationer, antingen tilldela barnraderna till rätt förälder eller ta bort de felaktiga raderna.
Efter varje migration, validera med några skrivningar som borde misslyckas: infoga en rad utan obligatoriskt värde, infoga en duplicerad nyckel, infoga ett värde utanför tillåtet intervall och referera en saknad föräldrarad. Misslyckade skrivningar är användbara signaler. De visar var appen tyst förlitade sig på "best effort"-beteende.
Föreställ dig ett litet CRM: accounts (varje kund till din SaaS), företag de arbetar med, kontakter på de företagen och deals kopplade till ett företag.
Det här är precis den typ av app folk snabbt genererar med ett chattverktyg. Den ser bra ut i demos, men verklig data blir rörig snabbt. Två buggar tenderar att dyka upp tidigt: dubbla kontakter (samma email inskriven två gånger på lite olika sätt) och deals skapade utan ett company eftersom en kodväg glömde sätta company_id. En annan klassiker är ett negativt deal-värde efter en refaktor eller en parsning som gått fel.
Fixen är inte fler if-satser. Det är några välvalda constraints som gör det omöjligt att lagra dåliga data.
-- 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;
Det handlar inte om att vara strikt för striktets skull. Du förvandlar vaga förväntningar till regler som databasen kan upprätthålla varje gång, oavsett vilken del av appen som skriver data.
När dessa constraints är på plats blir appen enklare. Du kan ta bort många defensiva kontroller som försöker upptäcka dubbletter i efterhand. Fel blir tydliga och åtgärdbara (till exempel "email finns redan för det här kontot" istället för konstigt efterföljande beteende). Och när en genererad API-rutt glömmer ett fält eller hanterar ett värde fel så misslyckas skrivningen omedelbart istället för att tyst korrupta databasen.
Constraints fungerar bäst när de matchar hur verksamheten faktiskt fungerar. Det mesta av smärtan kommer från att lägga till regler som känns "säkra" i stunden men blir överraskningar senare.
En vanlig fälla är att använda ON DELETE CASCADE överallt. Det ser prydligt ut tills någon tar bort en förälderrad och databasen tar bort halva systemet. Cascades kan vara rätt för verkligen ägd data (som utkast till radposter som aldrig ska finnas ensamma), men de är riskfyllda för poster folk ser som viktiga (kunder, fakturor, tickets). Om du är osäker, föredra RESTRICT och hantera raderingar avsiktligt.
Ett annat problem är att skriva CHECK-regler som är för snäva. "Status måste vara 'new', 'won' eller 'lost'" låter bra tills du behöver "paused" eller "archived". En bra CHECK-bestämmelse beskriver en stabil sanning, inte ett tillfälligt UI-val. "amount >= 0" åldras väl. "country in (...)" gör det ofta inte.
Några återkommande problem när team lägger till constraints efter att genererad kod redan körs i produktion:
CASCADE som ett städhjälpmedel och sedan radera mer data än tänkt.Om prestanda: PostgreSQL skapar automatiskt ett index för UNIQUE, men foreign keys indexerar inte automatiskt den refererande kolumnen. Utan det indexet kan uppdateringar och borttagningar på föräldern bli långsamma eftersom Postgres måste skanna child-tabellen för att kontrollera referenser.
Innan du skärper en regel, hitta befintliga rader som skulle misslyckas med den, bestäm om du ska fixa eller karantänera dem och rulla ut ändringen i steg.
Innan du skickar, ta fem minuter per tabell och skriv ner vad som alltid måste vara sant. Om du kan säga det på vanlig svenska kan du oftast upprätthålla det med en constraint.
Ställ dessa frågor för varje tabell:
Om du använder ett chattstyrt byggverktyg, behandla dessa invariants som acceptanskriterier för datan, inte som valfria anteckningar. Till exempel: "En deal-summa måste vara icke-negativ", "En kontakt-e-post är unik per workspace", "En uppgift måste referera en riktig kontakt." Ju tydligare reglerna är, desto mindre utrymme finns för oavsiktliga kantfall.
Koder.ai (koder.ai) inkluderar funktioner som planeringsläge, snapshots och rollback samt export av källkod, vilket kan göra det enklare att iterera schemaändringar säkert medan du skärper constraints över tid.
Ett enkelt roll-out-mönster som fungerar i verkliga team: välj en högvärdestabell (users, orders, invoices, contacts), lägg till 1–2 constraints som förhindrar de värsta felen (ofta NOT NULL och UNIQUE), fixa de skrivningar som misslyckas, och upprepa. Att skärpa regler över tid slår en stor och riskfylld migration.