PostgreSQL kısıtlamalarını (NOT NULL, CHECK, UNIQUE, FOREIGN KEY) kullanarak AI kaynaklı kenar-durum hatalarını erkenden durdurun ve daha güvenli uygulamalar yayınlayın.

AI tarafından yazılan kod genellikle mutlu yolu (happy path) iyi ele aldığı için doğru görünür. Gerçek uygulamalar ise orta kısımda başarısız olur: bir form null yerine boş string gönderir, bir arka plan işi tekrarlar ve aynı kaydı iki kez oluşturur veya bir silme işlemi ebeveyni kaldırıp çocukları ortada bırakır. Bunlar egzotik hatalar değildir. Zorunlu alanların boş görünmesi, tekrar eden "benzersiz" değerler ve hiçbir şeye işaret etmeyen yetim satırlar olarak ortaya çıkarlar.
Bu hatalar kod incelemesinden ve temel testlerden de sıyrılır; sebep basit: inceleyen kişi niyeti okur, her köşe durumunu değil. Testler genelde birkaç tipik örneği kapsar, haftalar süren gerçek kullanıcı davranışını, CSV importlarını, dalgalanan ağ yeniden denemelerini ya da eşzamanlı istekleri değil. Eğer bir asistan kod ürettiyse, boşlukları kırpma, aralık doğrulama veya yarış koşullarına karşı koruma gibi küçük ama kritik kontrolleri atlayabilir.
"Önce kısıtlamalar, sonra kod" yaklaşımı, vazgeçilemez kuralları veritabanına koymanız anlamına gelir; böylece hangi kod yolu yazmaya çalışırsa çalışsın kötü veri saklanamaz. Uygulamanız hala daha iyi hata mesajları için girdiyi doğrulamalı, ama doğruluk veritabanı tarafından zorlanmalıdır. PostgreSQL kısıtlamaları tam da burada parlıyor: sizi birçok hata kategorisinden korurlar.
Küçük bir örnek: küçük bir CRM düşünün. AI tarafından üretilen bir import betiği kişiler (contacts) yaratıyor. Bir satırın email'i "" (boş), iki satır aynı email'i farklı büyük-küçük harf ile tekrar ediyor ve bir kişi account_id referans ediyor ama o hesap başka bir süreçte silinmiş. Kısıtlama yoksa bunların hepsi üretime iner ve sonradan raporları bozar.
Doğru veritabanı kuralları ile bu yazmalar hemen başarısız olur; kaynağa yakın bir yerde hata verir. Gerekli alanlar eksik olamaz, tekrarlar yeniden denemelerde sızamaz, ilişkiler silinmiş veya var olmayan kayıtlara işaret edemez ve değerler izin verilen aralıkların dışında olamaz.
Kısıtlamalar her hatayı önlemez. Karışık bir kullanıcı arayüzünü, yanlış bir indirim hesaplamasını veya yavaş bir sorguyu düzeltmezler. Ama kötü verinin sessizce birikmesini durdururlar; bu genellikle "AI tarafından üretilmiş kenar-durum hatalarının" maliyetli olduğu yerdir.
Uygulamanız nadiren tek bir kod tabanı ile tek bir kullanıcı arasında olur. Tipik bir ürün web UI, mobil uygulama, yönetici ekranları, arka plan işleri, CSV importları ve bazen üçüncü taraf entegrasyonları içerir. Her yol veri oluşturup değiştirebilir. Eğer her yol aynı kuralları hatırlamak zorundaysa, bir tanesi unutacaktır.
Veritabanı hepsinin paylaştığı tek yerdir. Bunu son bekçi olarak ele aldığınızda kurallar her şey için otomatik olarak uygulanır. PostgreSQL kısıtlamaları "bunun her zaman doğru olduğunu varsayıyoruz"u "bu doğru olmalı, yoksa yazma başarısız olsun"a çevirir.
AI tarafından üretilen kod bunu daha da önemli kılar. Bir model React UI'da form doğrulaması ekleyebilir ama bir arka plan işindeki bir köşe durumunu atlayabilir. Ya da mutlu yol verisini iyi işler, sonra gerçek bir müşteri beklenmedik bir şey girince bozulur. Kısıtlamalar kötü veri veritabanına girmeye çalıştığı anda hatayı yakalar, haftalar sonra garip raporları debug yaparken değil.
Kısıtlamaları atladığınızda kötü veri genellikle sessizdir. Kaydetme başarılı olur, uygulama ilerler ve sorun daha sonra bir destek talebi, fatura uyumsuzluğu veya kimsenin güvenmediği bir gösterge panosu olarak ortaya çıkar. Temizleme maliyetlidir çünkü geçmişi düzeltirsiniz, tek bir isteği değil.
Kötü veri genelde şu günlük durumlarla girer: yeni bir istemci uygulama alanı eksik yerine boş gönderir, bir yeniden deneme tekrarlar ve çoğaltma oluşturur, bir yönetici düzenlemesi UI kontrollerini atlar, bir import dosyası tutarsız format içerir veya iki kullanıcı ilişkili kayıtları aynı anda günceller.
Kullanışlı bir zihinsel model: veriyi sınırda (boundary) sadece geçerliyse kabul edin. Pratikte o sınır veritabanını da içermelidir, çünkü veritabanı tüm yazmaları görür.
NOT NULL en basit PostgreSQL kısıtlamasıdır ve şaşırtıcı derecede geniş bir hata sınıfını önler. Bir değer satır için zorunluysa, veritabanının bunu zorlamasını sağlayın.
NOT NULL genellikle tanımlayıcılar, zorunlu isimler ve zaman damgaları için doğrudur. Geçerli bir kayıt bu olmadan oluşturulamıyorsa, boş olmasına izin vermeyin. Küçük bir CRM'de sahibi veya oluşturulma zamanı olmayan bir lead "kısmi lead" değil; ileride garip davranışlara yol açacak bozuk veridir.
NULL, AI tarafından üretilen kodla daha kolay sızar çünkü fark etmeden "opsiyonel" yollar oluşturmak kolaydır. Bir form alanı UI'da isteğe bağlı olabilir, bir API eksik anahtarı kabul edebilir ve create fonksiyonunun bir dalı bir değer atlamayı unutabilir. Her şey derlenir ve mutlu yol testi geçer. Sonra gerçek kullanıcı CSV ile boş hücreler import eder veya mobil istemci farklı bir yük gönderir ve NULL veritabanına düşer.
Sistem tarafından yönetilen alanlar için NOT NULL ile anlamlı bir varsayılanı birleştirmek iyi bir pattır:
created_at TIMESTAMP NOT NULL DEFAULT now()status TEXT NOT NULL DEFAULT 'new'is_active BOOLEAN NOT NULL DEFAULT trueVarsayılanlar her zaman kazan değildir. email veya company_name gibi kullanıcı tarafından sağlanan alanları sadece NOT NULL'u sağlamak için varsayılanda yapmayın. Boş string NULL'dan "daha geçerli" değildir; sadece problemi gizler.
Emin olmadığınızda, değerin gerçekten bilinmiyor mu yoksa farklı bir durumu mu temsil ettiğine karar verin. "Henüz sağlanmadı" anlamlıysa, her yerde NULL'a izin vermek yerine ayrı bir durum sütunu düşünün. Örneğin phone nullable olsun, ama phone_status ekleyin: missing, requested veya verified. Bu, anlamı kod genelinde tutarlı kılar.
Bir CHECK kısıtlaması tablonuzun verdiği bir sözdür: her satır her zaman bir kuralı sağlamalıdır. Bu, kenar durumlarının sessizce kod içinde makul görünen ama gerçek hayatta anlam ifade etmeyen kayıtlar oluşturmasını engellemenin en kolay yollarından biridir.
CHECK kısıtlamaları aynı satırdaki değerlere bağımlı kurallar için en iyisidir: sayısal aralıklar, izin verilen değerler ve sütunlar arası basit ilişkiler.
-- 1) Totals should never be negative
ALTER TABLE invoices
ADD CONSTRAINT invoices_total_nonnegative
CHECK (total_cents \u003e= 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 \u003e= start_date);
İyi bir CHECK kolay okunur. Bunu veriniz için bir dokümantasyon gibi düşünün. Kısa ifadeleri, açık kısıtlama adlarını ve öngörülebilir desenleri tercih edin.
CHECK her şey için doğru araç değildir. Bir kural başka satırlara bakmayı, agregasyon yapmayı veya tablolar arası karşılaştırma gerektiriyorsa (örneğin "bir hesabın plan limiti aşılamaması"), o mantığı uygulama kodunda, tetikleyicilerde veya kontrollü bir arka plan işinde tutun.
UNIQUE kuralı basittir: veritabanı sizin tanımladığınız sütunda aynı değere sahip iki satırı saklamayı reddeder. Bu, "oluştur" yolunun iki kez çalıştığı, bir yeniden denemenin gerçekleştiği veya iki kullanıcının aynı şeyi aynı anda gönderdiği durumlarda ortaya çıkan birçok hatayı siler.
UNIQUE, tanımladığınız tam değerler için tekrarları garanti eder. Değerin mevcut olduğunu (NOT NULL), biçimini takip ettiğini (CHECK) veya sizin eşitlik anlayışınıza uyduğunu (büyük-küçük harf, boşluk, noktalama) garanti etmez — bunları tanımlamazsanız.
Sıklıkla benzersiz olmasını istediğiniz yerler: kullanıcı tablosunda email, başka bir sistemden gelen external_id veya (account_id, name) gibi bir hesap içinde benzersiz olması gereken bir isim.
Bir durum: NULL ve UNIQUE. PostgreSQL'de NULL "bilinmiyor" olarak kabul edilir, bu yüzden UNIQUE kısıtlaması altında birden çok NULL değere izin verilir. Eğer "değer mevcut olmalı ve benzersiz olmalı" diyorsanız, UNIQUE ile NOT NULL'u birleştirin.
Kullanıcıya dönük tanımlayıcılar için pratik bir desen büyük-küçük harf duyarsız benzersizliktir. İnsanlar "[email protected]" ve sonra "[email protected]" yazacaktır ve bunların aynı olmasını bekler.
-- 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);
"Çoğaltma"nın kullanıcılarınız için ne anlama geldiğini (büyük-küçük harf, boşluk, hesap bazlı mı global mi) tanımlayın, sonra bunu bir kez kodlayın ki her yol aynı kuralı izlesin.
Bir FOREIGN KEY der ki: "bu satır oradaki gerçek bir satıra işaret etmelidir." Bunu olmadan kod sessizce izole bakıldığında geçerli görünen ama uygulamayı sonradan bozan yetim kayıtlar oluşturabilir. Örneğin: silinmiş bir müşteriye işaret eden bir not veya hiç var olmamış bir kullanıcı ID'sine işaret eden bir fatura.
Yabancı anahtarlar özellikle iki eylem birbirine yakın olduğunda önem kazanır: bir silme ve bir oluşturma, bir zaman aşımı sonrası yeniden deneme veya eski verilerle çalışan bir arka plan işi. Veritabanı tutarlılığı uygulama yollarının her birinin hatırlamasından daha iyi uygular.
ON DELETE seçeneği ilişkinin gerçek dünya anlamıyla eşleşmelidir. Sorun: "Ebeveyn kaybolursa, çocuk var olmaya devam etmeli mi?"
RESTRICT (veya NO ACTION): çocuklar varsa ebeveynin silinmesini engelle.CASCADE: ebeveyn silindiğinde çocukları da sil.SET NULL: çocuğu tut ama bağlantıyı kaldır.CASCADE ile dikkatli olun. Doğru olabilir, ama bir bug ya da yönetici işlemi ebeveyni silince beklediğinizden daha fazlasını silebilir.
Multi-tenant uygulamalarda yabancı anahtarlar sadece doğruluk için değil. Ayrıca hesaplar arası sızıntıyı önlerler. Yaygın bir desen her tenant-a ait tabloda account_id bulundurmak ve ilişkileri onun üzerinden bağlamaktır.
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
);
Bu şema içinde "kimin neye sahip olduğu"nu zorlar: bir not farklı bir hesaptaki bir kontağa işaret edemez, uygulama kodu (veya bir LLM tarafından üretilmiş sorgu) denese bile.
Önce sabitlerin kısa bir listesini yazın: her zaman doğru olması gereken gerçekler. Bunları basit tutun. "Her iletişim bir email gerektirir." "Bir durum birkaç izin verilen değerden biri olmalı." "Bir fatura gerçek bir müşteriye ait olmalı." Bunlar veritabanının her seferinde zorlamasını istediğiniz kurallardır.
Değişiklikleri küçük migration'larla yayına alın ki prod sürpriz yaşamasın:
NOT NULL, UNIQUE, CHECK, FOREIGN KEY).Mevcut kötü veri işin en dağınık kısmıdır. Bunun için plan yapın. Çoğaltmalar için bir kazanan satır seçin, diğerlerini birleştirin ve küçük bir denetim notu tutun. Eksik zorunlu alanlar için ancak gerçekten güvenli ise güvenli bir varsayılan seçin; aksi halde karantinaya alın. Bozuk ilişkilerde ya çocuk satırları doğru ebeveyne atayın ya da kötü satırları kaldırın.
Her migration'dan sonra başarısız olması gereken birkaç yazma ile doğrulayın: zorunlu bir değer eksik bir satır eklemeyi deneyin, yinelemeli anahtar eklemeyi deneyin, aralık dışı bir değer ekleyin ve eksik bir ebeveyn satırına referans verin. Başarısız yazmalar faydalı sinyallerdir. Uygulamanın nerede "en iyi girişim" davranışına güvenerek sessizce hareket ettiğini gösterirler.
Küçük bir CRM hayal edin: hesaplar (SaaS müşterileriniz), onların çalıştığı şirketler, o şirketlerdeki kişiler ve anlaşmalar (deals) şirkete bağlı.
Bu, sohbet aracıyla hızlıca üretilen tipik bir uygulamadır. Demo'da iyi görünür, ama gerçek veri hızla dağınıklaşır. İki hata genelde erken ortaya çıkar: çoğaltılmış kişiler (aynı email biraz farklı girilmiş) ve company_id'siz oluşturulan anlaşmalar çünkü bir kod yolu company_id atamayı unutmuştur. Diğer klasik durum ise refaktör veya parsing hatası sonrası negatif anlaşma değeri.
Çözüm daha fazla if-dizesi değil. Kötü veriyi saklamayı imkansız kılan birkaçı iyi seçilmiş kısıtlamadır.
-- 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 \u003e= 0);
-- A few obvious required fields
ALTER TABLE companies
ALTER COLUMN name SET NOT NULL;
ALTER TABLE contacts
ALTER COLUMN email SET NOT NULL;
Bu katı olmak için değil. Belirsiz beklentileri veritabanının her yazmada zorlayabileceği kurallara çeviriyorsunuz.
Bu kısıtlamalar uygulandıktan sonra uygulama daha basit olur. Sonradan çoğaltmaları tespit etmeye çalışan birçok savunmacı kontrolü kaldırabilirsiniz. Hatalar net ve işlem yapılabilir hale gelir (örneğin "bu hesap için email zaten mevcut" yerine downstream'de tuhaf davranış). Ve üretilmiş bir API rotası bir alanı unutur ya da bir değeri yanlış ele alırsa, yazma hemen başarısız olur, veritabanını sessizce bozmak yerine.
Kısıtlamalar iş mantığıyla örtüştüğünde en iyi çalışır. Çoğu sıkıntı anlık olarak "güvenli" görünen ama sonra sürpriz yapan kurallar eklemekten gelir.
Yaygın bir hata ON DELETE CASCADE'i her yerde kullanmaktır. İlk bakışta düzenli görünür ama biri bir ebeveyni silince veritabanı sistemin yarısını silebilir. Taslak kalemleri (draft line items) gibi gerçekten yalnız başına olmaması gereken veriler için CASCADE doğru olabilir, ama müşteri, fatura, ticket gibi önemli kayıtlar için risklidir. Emin değilseniz RESTRICT tercih edin ve silmeleri kasıtlı şekilde ele alın.
Bir diğer problem çok dar CHECK kuralları yazmaktır. "Status 'new', 'won' veya 'lost' olmalı" kulağa iyi gelirken daha sonra "paused" veya "archived" gerektiğinde engel olur. İyi bir CHECK kalıcı bir gerçeği tanımlar, geçici UI seçimini değil. "amount \u003e= 0" uzun vadede iyi yaşar. "country in (...)" sık sık problem olur.
Ek olarak, kısıtlamalar uygulandıktan sonra sık görülen sorunlar:
CASCADE'ı temizlik aracı gibi kullanmak ve beklenenden fazla veri silinmesi.Performans konusunda: PostgreSQL UNIQUE için otomatik olarak bir indeks oluşturur, ama yabancı anahtarlar referans eden sütunu otomatik indekslemez. O indeks yoksa, ebeveyn üzerinde güncelleme ve silme yavaşlayabilir çünkü Postgres referansları kontrol etmek için child tabloyu taramak zorunda kalır.
Bir kuralı sıkılaştırmadan önce, o kuralı başarısız kılacak mevcut satırları bulun, düzeltip düzeltemeyeceğinize veya karantinaya alıp alamayacağınıza karar verin ve değişikliği adım adım yayınlayın.
Yayınlamadan önce her tablo için beş dakikanızı ayırıp her zaman doğru olması gerekenleri yazın. Bunu düz İngilizceyle söyleyebiliyorsanız, genellikle bir kısıtlama ile zorlanabilir.
Her tablo için şu soruları sorun:
Eğer sohbet tabanlı bir inşa aracı kullanıyorsanız, bu invariants'ları veri için kabul kriterleri olarak ele alın, isteğe bağlı notlar değil. Örneğin: "Bir deal miktarı negatif olamaz", "Bir contact email'i workspace başına benzersiz olmalı", "Bir görev gerçek bir contact'a referans etmeli." Kurallar ne kadar açık olursa, kazara kenar durumları için o kadar az boşluk kalır.
Koder.ai (koder.ai) planning mode, snapshot ve rollback ve kaynak kodu dışa aktarma gibi özellikler içerir; bu da şema değişikliklerini güvenle yineleyip zaman içinde kısıtlamaları sıkılaştırmayı kolaylaştırabilir.
Gerçek ekiplerde işe yarayan basit bir yayılma deseni: yüksek değerli bir tablo seçin (users, orders, invoices, contacts), en kötü hataları önleyecek 1-2 kısıtlama ekleyin (genelde NOT NULL ve UNIQUE), başarısız olan yazmaları düzeltin, sonra tekrarlayın. Kuralları zaman içinde sıkılaştırmak tek büyük riskli migration'dan daha iyidir.