Constraint & Data Integrity — Database

Database Constraints Constraints adalah aturan yang diterapkan pada kolom untuk memastikan data tetap valid dan konsisten. Database menolak data yang…

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)
);

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

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();

📌 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

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().

Yang akan kamu pelajari