Constraint adalah aturan yang ditempel di tabel, dan database sendiri yang menjaganya: data yang melanggar ditolak, dari mana pun datangnya. Bayangin gerbang tiket di stasiun MRT. Petugas di depan boleh mengingatkan "saldonya cukup, Kak?", tapi yang benar-benar menahan kartu tanpa saldo adalah palang gerbangnya, dan palang itu ada di setiap pintu masuk. Validasi di aplikasi itu petugasnya. Constraint itu palangnya.
🧪 Tebakan cepat: sebelum menyimpan ulasan, aplikasi Kedai Senja mengecek dulu "Budi sudah pernah mengulas Kopi Susu belum?". Budi menekan tombol Kirim dua kali dengan sangat cepat. Tanpa constraint di database, berapa ulasan Budi yang tersimpan: 1 atau 2? Tebak dulu, jawabannya ada di bagian "Kenapa cek di aplikasi saja gak cukup".
Tabel ulasan dengan aturannya
Kedai Senja punya tabel customers (pelanggan: Budi id 1, Siti id 2) dan products (Kopi Susu id 1, Kopi Susu Aren id 2). Sekarang pembeli boleh menulis ulasan. Aturannya: ulasan harus punya penulis dan produk yang benar-benar ada, rating 1 sampai 5, dan satu orang cuma boleh mengulas satu produk sekali. Semua aturan itu ditulis langsung di tabelnya:
PRAGMA foreign_keys = ON;
CREATE TABLE reviews (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL REFERENCES customers(id),
product_id INTEGER NOT NULL REFERENCES products(id),
rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5),
komentar TEXT,
status TEXT DEFAULT 'tampil',
UNIQUE (customer_id, product_id)
);
- NOT NULL: kolom wajib diisi.
NULLadalah tanda "gak ada isinya". - FOREIGN KEY (
REFERENCES customers(id)): nilainya harus ada di kolomidtabel lain, persis seperti di pelajaran Primary Key & Foreign Key. Di SQLite, pemeriksaannya baru jalan setelahPRAGMA foreign_keys = ON;. - CHECK (
CHECK (rating BETWEEN 1 AND 5)): aturan buatanmu sendiri. Kondisi di dalam kurung harus benar untuk setiap baris.BETWEEN 1 AND 5artinya 1 sampai 5, termasuk 1 dan 5. - DEFAULT (
DEFAULT 'tampil'): isian bawaan. KalauINSERTgak menyebut kolomstatus, isinya otomatis'tampil'. DEFAULT gak menolak apa pun, dia cuma mengisi. - UNIQUE (
UNIQUE (customer_id, product_id)): gak boleh ada dua baris yang sama. Karena isinya dua kolom, yang gak boleh kembar adalah pasangannya. Budi boleh mengulas Kopi Susu dan Kopi Susu Aren, Siti dan Budi boleh mengulas Kopi Susu, tapi pasangan Budi + Kopi Susu cuma boleh ada sekali.
Lima request nakal, lima penolakan
Ini request yang dikirim aplikasi ke tabel di atas, dengan jawaban SQLite apa adanya (sama dengan yang muncul di Lab):
INSERT INTO reviews (customer_id, product_id, rating, komentar) VALUES (2, 1, 5, 'Manisnya pas');
-- masuk, status otomatis 'tampil'
INSERT INTO reviews (customer_id, product_id, rating, komentar) VALUES (1, 1, 4, 'Mantap');
INSERT INTO reviews (customer_id, product_id, rating, komentar) VALUES (1, 1, 4, 'Mantap');
-- yang pertama masuk, yang kedua:
-- UNIQUE constraint failed: reviews.customer_id, reviews.product_id
INSERT INTO reviews (customer_id, product_id, rating, komentar) VALUES (1, 2, 6, 'Enak banget');
-- CHECK constraint failed: rating BETWEEN 1 AND 5
INSERT INTO reviews (product_id, rating) VALUES (2, 4);
-- NOT NULL constraint failed: reviews.customer_id
INSERT INTO reviews (customer_id, product_id, rating) VALUES (99, 2, 1);
-- FOREIGN KEY constraint failed
- Pesan error-nya menyebut jenis aturan yang dilanggar, lalu kolom atau kondisinya. Baca pesan itu dulu sebelum menebak-nebak.
INSERTyang ditolak batal seluruhnya. Gak ada baris setengah jadi yang tertinggal.- Hasil akhirnya cuma 2 baris: ulasan Siti dan ulasan pertama Budi. Empat data kotor gak pernah sampai ke tabel.
Kenapa cek di aplikasi saja gak cukup
Klik dua kali yang sangat cepat mengirim dua request yang jalan hampir bersamaan. Urutannya bisa begini:
| Waktu | Request A (klik pertama) | Request B (klik kedua) |
|---|---|---|
| 0 ms | cek: ulasan Budi untuk Kopi Susu ada? 0 | |
| 1 ms | cek: ulasan Budi untuk Kopi Susu ada? 0 | |
| 2 ms | INSERT, masuk |
|
| 3 ms | INSERT, masuk juga |
Dua-duanya mengecek sebelum ada yang menyimpan, jadi dua-duanya melihat 0. Tanpa constraint, ulasan Budi tersimpan 2. Itu jawaban tebakannya. Dengan UNIQUE (customer_id, product_id), database memeriksa tepat saat menyimpan, jadi INSERT kedua ditolak. Selain itu, data juga bisa masuk tanpa lewat form: script import, panel admin, atau query manual. Constraint menjaga semua jalan itu.
Cek di aplikasi tetap berguna untuk pesan yang ramah ("Kamu sudah mengulas produk ini"). Di Laravel, kalau INSERT-nya tetap kena UNIQUE, tangkap Illuminate\Database\UniqueConstraintViolationException lalu tampilkan pesan yang sama.
UNIQUE dan PRIMARY KEY
Dua-duanya menolak nilai kembar, tapi bedanya jelas:
| PRIMARY KEY | UNIQUE | |
|---|---|---|
Boleh NULL? |
Gak boleh | Boleh, kecuali ditambah NOT NULL |
| Berapa per tabel? | Cuma satu | Boleh banyak |
| Tugasnya | Identitas tiap baris | Aturan bisnis: email, pasangan pelanggan + produk |
Jadi PRIMARY KEY = UNIQUE + NOT NULL, dan cuma satu per tabel. Tabel customers punya primary key id, dan boleh punya UNIQUE di email dan di phone sekaligus. Dua-duanya juga otomatis dibuatkan index, jadi pencarian lewat kolom itu cepat.
Di migration Laravel
Schema::create('reviews', function (Blueprint $table) {
$table->id();
$table->foreignId('customer_id')->constrained();
$table->foreignId('product_id')->constrained();
$table->unsignedTinyInteger('rating');
$table->text('komentar')->nullable();
$table->string('status')->default('tampil');
$table->unique(['customer_id', 'product_id']);
});
// di tabel customers:
$table->string('email')->unique();
- Kolom Laravel otomatis
NOT NULL, kecuali ditulis->nullable(). ->constrained()membuat FOREIGN KEY ke tabelcustomersdanproducts.->unique()di satu kolom membuat UNIQUE untuk kolom itu.$table->unique([...])dengan array membuat UNIQUE gabungan.->default('tampil')membuat DEFAULT.- Blueprint Laravel belum punya method untuk CHECK. Di MySQL atau PostgreSQL, tulis lewat
DB::statement('ALTER TABLE reviews ADD CONSTRAINT reviews_rating_check CHECK (rating BETWEEN 1 AND 5)');.
📌 Pesan yang sama di PostgreSQL
Aturannya sama di semua database, cuma kalimatnya beda. PostgreSQL menamai constraint-nya dan menunjukkan datanya:
ERROR: duplicate key value violates unique constraint "reviews_customer_id_product_id_key"
DETAIL: Key (customer_id, product_id)=(1, 1) already exists.
ERROR: new row for relation "reviews" violates check constraint "reviews_rating_check"
ERROR: insert or update on table "reviews" violates foreign key constraint "reviews_customer_id_fkey"
DETAIL: Key (customer_id)=(99) is not present in table "customers".
PostgreSQL dan MySQL (InnoDB) selalu memeriksa FOREIGN KEY tanpa perlu PRAGMA. MySQL baru benar-benar menjalankan CHECK sejak versi 8.0.16, versi lebih lama menerimanya lalu diam-diam mengabaikannya.
⚠️ Jebakan yang sering ketemu
- CHECK lolos untuk NULL.
NULL BETWEEN 1 AND 5hasilnya "gak tahu", bukan "salah", jadi rating kosong tetap masuk. Kalau wajib ada, tambahkanNOT NULLjuga. - UNIQUE di kolom yang salah.
UNIQUE (customer_id)saja berarti Budi cuma boleh mengulas satu produk seumur hidup. Yang dimaksud adalah pasangan(customer_id, product_id). - Lupa
PRAGMA foreign_keys = ONdi SQLite. FOREIGN KEY-nya ada di tabel, tapi gak ada yang memeriksa. - Menambah constraint ke tabel yang sudah kotor. Kalau sudah ada ulasan dobel, membuat UNIQUE-nya gagal. Bersihkan datanya dulu, baru pasang aturannya.
- Di latihan, ejaan nama constraint harus persis: huruf besar, dengan spasi di
"NOT NULL"dan"FOREIGN KEY".
TL;DR: Constraint adalah aturan di tabel yang dijaga database sendiri, jadi data kotor ditolak dari jalan mana pun. NOT NULL wajib diisi, UNIQUE gak boleh kembar (dengan dua kolom, pasangannya yang gak boleh kembar), CHECK (kondisi) harus benar, DEFAULT mengisi kolom yang gak disebut, dan FOREIGN KEY (REFERENCES) memastikan nilainya ada di tabel lain. Cek di aplikasi bisa kebobolan oleh dua request yang bersamaan, constraint enggak. PRIMARY KEY = UNIQUE + NOT NULL, cuma satu per tabel. Di Laravel: ->unique(), $table->unique([...]), ->constrained(), ->default().