Kirim aplikasi AI-generated yang lebih aman dengan mengandalkan constraint PostgreSQL untuk NOT NULL, CHECK, UNIQUE, dan FOREIGN KEY sebelum kode dan tes.

Kode yang dihasilkan AI sering terlihat benar karena menangani jalur yang ideal. Aplikasi nyata gagal di tengah yang berantakan: sebuah form mengirim string kosong bukannya null, job latar belakang mencoba ulang dan membuat record yang sama dua kali, atau penghapusan menghapus baris induk dan meninggalkan anak yatim. Ini bukan bug eksotis. Mereka muncul sebagai field wajib kosong, nilai “unik” ganda, dan baris yatim yang menunjuk ke mana pun.
Mereka juga lolos dari code review dan tes dasar karena alasan sederhana: reviewer membaca niat, bukan setiap edge case. Tes biasanya mencakup beberapa contoh khas, bukan minggu-minggu perilaku pengguna nyata, impor dari CSV, retry jaringan yang fluktuatif, atau permintaan bersamaan. Jika seorang assistant menghasilkan kode, ia bisa melewatkan pengecekan kecil tapi krusial seperti memangkas spasi, memvalidasi rentang, atau melindungi terhadap kondisi race.
“Constraints first, code second” berarti Anda menempatkan aturan yang tidak bisa dinegosiasikan di database sehingga data buruk tidak bisa disimpan, tidak peduli jalur kode mana yang mencoba menulisnya. Aplikasi Anda tetap harus memvalidasi input untuk pesan error yang lebih baik, tapi database menegakkan kebenaran. Di sinilah PostgreSQL constraints bersinar: mereka melindungi Anda dari kategori kesalahan secara keseluruhan.
Contoh cepat: bayangkan CRM kecil. Skrip impor yang dihasilkan AI membuat kontak. Satu baris memiliki email "" (kosong), dua baris mengulangi email yang sama dengan kapitalisasi berbeda, dan satu kontak merujuk account_id yang tidak ada karena akun tersebut dihapus oleh proses lain. Tanpa constraint, semua itu bisa mendarat di produksi dan merusak laporan nanti.
Dengan aturan database yang tepat, penulisan itu gagal segera, dekat sumbernya. Field wajib tidak bisa hilang, duplikat tidak bisa menyelinap saat retry, relasi tidak bisa menunjuk ke record yang dihapus atau tidak ada, dan nilai tidak bisa berada di luar rentang yang diizinkan.
Constraint tidak mencegah semua bug. Mereka tidak akan memperbaiki UI yang membingungkan, perhitungan diskon yang salah, atau query yang lambat. Tetapi mereka menghentikan data buruk menumpuk diam-diam, yang seringkali menjadi tempat “bug edge-case yang dihasilkan AI” menjadi mahal.
Aplikasi Anda jarang hanya satu codebase yang berkomunikasi dengan satu pengguna. Produk khas memiliki UI web, aplikasi mobile, layar admin, job latar belakang, impor dari CSV, dan kadang integrasi pihak ketiga. Setiap jalur bisa membuat atau mengubah data. Jika setiap jalur harus mengingat aturan yang sama, salah satunya akan lupa.
Database adalah tempat yang mereka semua bagi. Saat Anda memperlakukannya sebagai penjaga gerbang akhir, aturan berlaku otomatis untuk semuanya. Constraint PostgreSQL mengubah “kita berasumsi ini selalu benar” menjadi “ini harus benar, atau penulisan gagal.”
Kode yang dihasilkan AI membuat ini jadi lebih penting. Model mungkin menambahkan validasi form di UI React tapi melewatkan corner case di job latar belakang. Atau mungkin menangani data jalur ideal dengan baik, lalu gagal ketika pelanggan nyata memasukkan sesuatu yang tak terduga. Constraint menangkap masalah pada saat data buruk mencoba masuk, bukan berminggu-minggu kemudian saat Anda mendebug laporan aneh.
Saat Anda melewatkan constraint, data buruk seringkali senyap. Simpan berhasil, aplikasi melanjutkan, dan masalah muncul nanti sebagai tiket dukungan, mismatch penagihan, atau dashboard yang tidak dapat dipercaya. Pembersihan mahal karena Anda memperbaiki sejarah, bukan satu permintaan.
Data buruk biasanya menyelinap lewat situasi sehari-hari: versi klien baru mengirim field sebagai kosong bukannya hilang, retry membuat duplikat, edit admin melewati pengecekan UI, file impor punya format inkonsisten, atau dua pengguna memperbarui record terkait pada saat bersamaan.
Model mental yang berguna: terima data hanya jika valid di batasnya. Dalam praktik, batas itu harus mencakup database, karena database melihat semua penulisan.
NOT NULL adalah constraint PostgreSQL yang paling sederhana, dan mencegah kelas bug yang mengejutkan besar. Jika sebuah nilai harus ada agar baris masuk akal, biarkan database menegakkannya.
NOT NULL biasanya tepat untuk identifier, nama yang wajib, dan timestamp. Jika Anda tidak bisa membuat record yang valid tanpa itu, jangan izinkan kosong. Di CRM kecil, lead tanpa pemilik atau waktu dibuat bukanlah “lead parsial.” Itu data rusak yang akan menyebabkan perilaku aneh nanti.
NULL lebih mudah masuk dengan kode yang dihasilkan AI karena gampang membuat jalur “opsional” tanpa menyadarinya. Field form mungkin opsional di UI, API bisa menerima kunci yang hilang, dan satu cabang fungsi create bisa lupa menetapkan nilai. Semua masih kompilasi dan tes jalur ideal lulus. Lalu pengguna nyata mengimpor CSV dengan sel kosong, atau klien mobile mengirim payload berbeda, dan NULL mendarat di database.
Pola yang baik adalah menggabungkan NOT NULL dengan default yang masuk akal untuk field yang dimiliki sistem:
created_at TIMESTAMP NOT NULL DEFAULT now()status TEXT NOT NULL DEFAULT 'new'is_active BOOLEAN NOT NULL DEFAULT trueDefault tidak selalu menang. Jangan beri default pada field yang disediakan pengguna seperti email atau company_name hanya untuk memenuhi NOT NULL. String kosong bukan “lebih valid” daripada NULL. Itu hanya menyembunyikan masalah.
Jika ragu, putuskan apakah nilai itu benar-benar tidak diketahui, atau apakah ia merepresentasikan keadaan berbeda. Jika “belum diberikan” bermakna, pertimbangkan kolom status terpisah daripada mengizinkan NULL di mana-mana. Misalnya, biarkan phone nullable, tetapi tambahkan phone_status seperti missing, requested, atau verified. Itu menjaga makna konsisten di seluruh kode Anda.
Constraint CHECK adalah janji yang dibuat tabel Anda: setiap baris harus memenuhi aturan, setiap saat. Ini salah satu cara termudah untuk mencegah edge case dari membuat record yang diam-diam tampak baik di kode tapi tidak masuk akal di kenyataan.
CHECK paling cocok untuk aturan yang hanya bergantung pada nilai di baris yang sama: rentang numerik, nilai yang diizinkan, dan relasi sederhana antar kolom.
-- 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);
CHECK yang baik mudah dibaca sekilas. Perlakukan itu seperti dokumentasi untuk data Anda. Utamakan ekspresi singkat, nama constraint yang jelas, dan pola yang dapat diprediksi.
CHECK bukan alat yang tepat untuk segala hal. Jika sebuah aturan perlu melihat baris lain, agregat data, atau membandingkan antar tabel (misalnya, “sebuah akun tidak boleh melebihi batas plan-nya”), simpan logika itu di kode aplikasi, trigger, atau job latar belakang yang terkendali.
Aturan UNIQUE sederhana: database menolak menyimpan dua baris yang memiliki nilai sama di kolom yang dibatasi (atau kombinasi nilai yang sama di beberapa kolom). Ini menghapus seluruh kelas bug di mana jalur “create” berjalan dua kali, retry terjadi, atau dua pengguna mengirim hal yang sama pada saat bersamaan.
UNIQUE menjamin tidak ada duplikat untuk nilai tepat yang Anda definisikan. Ia tidak menjamin bahwa nilai hadir (NOT NULL), bahwa nilainya mengikuti format (CHECK), atau bahwa ia cocok dengan ide Anda tentang kesetaraan (huruf besar/kecil, spasi, tanda baca) kecuali Anda mendefinisikannya.
Tempat umum yang biasanya ingin Anda unik-kan termasuk email pada tabel users, external_id dari sistem lain, atau nama yang harus unik dalam sebuah akun seperti (account_id, name).
Satu jebakan: NULL dan UNIQUE. Di PostgreSQL, NULL diperlakukan sebagai “tidak diketahui,” sehingga beberapa NULL diperbolehkan di bawah constraint UNIQUE. Jika maksud Anda “nilai harus ada dan harus unik,” gabungkan UNIQUE dengan NOT NULL.
Pola praktis untuk identifier yang dilihat pengguna adalah unik case-insensitive. Orang akan mengetikkan “[email protected]” dan nanti “[email protected]” dan berharap itu sama.
-- 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);
Definisikan apa arti “duplikat” untuk pengguna Anda (case, spasi, per-akun vs global), lalu enkode itu sekali sehingga setiap jalur kode mengikuti aturan yang sama.
FOREIGN KEY mengatakan, “baris ini harus menunjuk ke baris nyata di sana.” Tanpanya, kode bisa diam-diam membuat record yatim yang tampak valid sendiri tapi merusak aplikasi nanti. Misalnya: catatan yang mereferensi customer yang dihapus, atau invoice yang menunjuk ke user ID yang tidak pernah ada.
Foreign key paling penting saat dua aksi terjadi berdekatan: penghapusan dan pembuatan, retry setelah timeout, atau job latar belakang berjalan dengan data kadaluarsa. Database lebih baik menegakkan konsistensi daripada setiap jalur aplikasi harus selalu ingat memeriksa.
Opsi ON DELETE harus cocok dengan makna dunia nyata dari relasi. Tanyakan: “Jika induk hilang, apakah anak tetap boleh ada?”
RESTRICT (atau NO ACTION): blokir penghapusan induk jika anak ada.CASCADE: menghapus induk akan menghapus anak juga.SET NULL: biarkan anak ada tapi hapus tautannya.Hati-hati dengan CASCADE. Bisa benar, tapi juga bisa menghapus lebih banyak dari yang Anda harapkan ketika bug atau tindakan admin menghapus record induk.
Di aplikasi multi-tenant, foreign key bukan hanya tentang kebenaran. Mereka juga mencegah kebocoran lintas akun. Pola umum adalah menyertakan account_id pada setiap tabel yang dimiliki tenant dan mengikat relasi melalui itu.
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
);
Ini menegakkan “siapa punya apa” di skema: sebuah note tidak bisa menunjuk ke contact di akun lain, bahkan jika kode aplikasi (atau query yang dihasilkan LLM) mencoba.
Mulailah dengan menulis daftar singkat invariant: fakta yang harus selalu benar. Buat sesederhana mungkin. “Setiap contact butuh email.” “Status harus salah satu dari beberapa nilai yang diizinkan.” “Invoice harus milik customer nyata.” Ini adalah aturan yang ingin Anda biarkan database tegakkan setiap saat.
Gulirkan perubahan dalam migrasi kecil agar produksi tidak terkejut:
NOT NULL, UNIQUE, CHECK, FOREIGN KEY).Bagian berantakan adalah data buruk yang sudah ada. Rencanakan itu. Untuk duplikat, pilih baris yang menang, gabungkan sisanya, dan simpan catatan audit kecil. Untuk field wajib yang hilang, pilih default yang aman hanya jika benar-benar aman; kalau tidak, karantina. Untuk relasi rusak, baik tetapkan ulang baris anak ke induk yang benar atau hapus baris yang buruk.
Setelah setiap migrasi, validasi dengan beberapa penulisan yang seharusnya gagal: masukkan baris dengan nilai wajib yang hilang, masukkan duplikat key, masukkan nilai di luar rentang, dan referensikan baris induk yang hilang. Penulisan yang gagal adalah sinyal berguna. Mereka menunjukkan di mana aplikasi diam-diam mengandalkan perilaku “usaha terbaik.”
Bayangkan CRM kecil: accounts (setiap pelanggan SaaS Anda), perusahaan yang mereka ajak bekerja, kontak di perusahaan itu, dan deals yang terkait ke perusahaan.
Ini persis jenis aplikasi yang orang bangun cepat dengan alat chat. Tampak baik di demo, tapi data nyata cepat berantakan. Dua bug yang cenderung muncul dini: kontak duplikat (email yang sama dimasukkan dua kali dengan cara sedikit berbeda), dan deals dibuat tanpa company karena satu jalur kode lupa menyetel company_id. Klasik lain adalah nilai deal negatif setelah refactor atau kesalahan parsing.
Perbaikannya bukan lebih banyak if-statement. Ini beberapa constraint yang dipilih dengan baik yang membuat data buruk tidak mungkin disimpan.
-- 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;
Ini bukan soal ketat demi ketat. Anda mengubah ekspektasi yang samar menjadi aturan yang bisa ditegakkan database setiap saat, tidak peduli bagian mana dari aplikasi yang menulis data.
Setelah constraint ini ada, aplikasi jadi lebih sederhana. Anda bisa menghapus banyak pengecekan defensif yang mencoba mendeteksi duplikat setelah fakta. Kegagalan menjadi jelas dan dapat ditindaklanjuti (misalnya, “email sudah ada untuk akun ini” daripada perilaku downstream yang aneh). Dan ketika route API yang dihasilkan lupa field atau salah menangani nilai, penulisan gagal segera daripada merusak database secara diam-diam.
Constraint bekerja terbaik ketika cocok dengan cara bisnis sebenarnya bekerja. Sebagian besar masalah muncul dari menambahkan aturan yang terasa “aman” saat itu tapi menjadi kejutan nanti.
Trap umum adalah menggunakan ON DELETE CASCADE di mana-mana. Terlihat rapi sampai seseorang menghapus induk dan database menghapus setengah sistem. Cascade bisa tepat untuk data yang benar-benar dimiliki (seperti draft line items yang tidak pernah boleh ada sendiri), tapi berisiko untuk record yang dianggap penting (customer, invoice, ticket). Jika tidak yakin, pilih RESTRICT dan tangani penghapusan dengan sengaja.
Masalah lain adalah menulis CHECK yang terlalu sempit. “Status harus ‘new’, ‘won’, atau ‘lost’” terdengar wajar sampai Anda butuh “paused” atau “archived.” CHECK yang baik menggambarkan kebenaran yang stabil, bukan pilihan UI sementara. “amount >= 0” bertahan lama. “country in (...)” sering tidak.
Beberapa isu yang sering muncul ketika tim menambahkan constraint setelah kode yang dihasilkan sudah berjalan:
CASCADE sebagai alat pembersih, lalu menghapus lebih banyak data dari yang dimaksud.Tentang performa: PostgreSQL otomatis membuat indeks untuk UNIQUE, tapi foreign key tidak otomatis mengindeks kolom yang mereferensikan. Tanpa indeks itu, update dan delete pada parent bisa melambat karena Postgres harus memindai tabel anak untuk memeriksa referensi.
Sebelum mengetatkan aturan, temukan baris yang ada yang akan gagal, putuskan memperbaiki atau mengarantinanya, dan rollout perubahan bertahap.
Sebelum Anda kirim, luangkan lima menit per tabel dan tuliskan apa yang harus selalu benar. Jika Anda bisa mengatakannya dalam bahasa Inggris biasa, biasanya Anda bisa menegakkannya dengan constraint.
Tanyakan untuk setiap tabel:
Jika Anda menggunakan alat build yang digerakkan chat, perlakukan invariant tersebut sebagai kriteria penerimaan untuk data, bukan catatan opsional. Misalnya: “Jumlah deal harus non-negatif,” “Email contact unik per workspace,” “Tugas harus merujuk ke contact nyata.” Semakin eksplisit aturannya, semakin sedikit ruang bagi edge case terjadi secara tidak sengaja.
Koder.ai (koder.ai) termasuk fitur seperti mode perencanaan, snapshot dan rollback, serta eksport source code, yang dapat mempermudah iterasi perubahan skema dengan aman sambil Anda mengetatkan constraint seiring waktu.
Pola rollout sederhana yang bekerja di tim nyata: pilih satu tabel bernilai tinggi (users, orders, invoices, contacts), tambahkan 1-2 constraint yang mencegah kegagalan terburuk (sering NOT NULL dan UNIQUE), perbaiki penulisan yang gagal, lalu ulangi. Mengetatkan aturan secara bertahap mengungguli satu migrasi besar yang berisiko.