Primary key adalah kolom (atau gabungan kolom) yang nilainya unik di setiap baris, jadi satu baris bisa ditunjuk dengan pasti. Foreign key adalah kolom yang isinya primary key tabel lain: penunjuk dari baris anak ke baris induknya. Bayangin kartu member Kedai Senja: nomornya unik dan gak pernah berubah walau pemiliknya ganti nama (primary key). Setiap nota cuma mencatat nomor member itu (foreign key). Pertanyaan besarnya muncul waktu seseorang menutup kartu membernya: nota-notanya dibuang, dicoret jadi "tamu", atau penutupan kartunya ditolak?
🧪 Tebakan cepat: Siti punya 2 pesanan. Foreign key
orders.customer_idditulis denganON DELETE SET NULL. Kamu menghapus Siti daricustomers. Apa yang terjadi pada 2 pesanannya: ikut terhapus, tetap ada tanpa pemilik, atauDELETE-nya ditolak? Tebak dulu, jawabannya ada di bagian "ON DELETE: nasib baris anak".
Primary key: KTP setiap baris
Primary key punya dua sifat: unik dan gak boleh kosong (NULL). Satu tabel cuma punya satu primary key. Coba masukkan pelanggan dengan id yang sudah dipakai Siti:
INSERT INTO customers (id, name) VALUES (2, 'Rina');
-- UNIQUE constraint failed: customers.id
Ada empat cara memilih primary key:
| Jenis | Contoh | Kapan dipakai |
|---|---|---|
| Auto-increment | id INTEGER PRIMARY KEY (SQLite), AUTO_INCREMENT (MySQL), GENERATED ALWAYS AS IDENTITY (PostgreSQL) |
pilihan paling umum, angkanya diisi database: 1, 2, 3 |
| UUID | '3f2b9c1e-...', dibuat aplikasi atau gen_random_uuid() di PostgreSQL |
id dibuat di banyak server, atau id gak boleh bisa ditebak dari URL |
| Natural key | kode negara 'ID', 'JP' |
nilai dari dunia nyata yang dijamin unik dan gak pernah berubah |
| Composite key | PRIMARY KEY (order_id, product_id) di order_items |
tabel penghubung, seperti di pelajaran 2NF |
Pilih nilai yang gak pernah berubah. Email dan nomor HP kelihatannya unik, tapi bisa diganti pemiliknya, dan setiap foreign key yang menunjuknya harus ikut diubah. Karena itu kebanyakan tabel memakai id auto-increment.
Foreign key: penunjuk ke primary key lain
Di pelajaran SQL Relasi Antar Tabel kamu sudah menulis foreign key. Ini tabel orders Kedai Senja, sekarang dengan aturan hapusnya:
PRAGMA foreign_keys = ON;
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id) ON DELETE SET NULL,
total INTEGER NOT NULL
);
INSERT INTO customers VALUES (1, 'Budi'), (2, 'Siti'), (3, 'Andi');
INSERT INTO orders VALUES (1, 1, 56000), (2, 1, 22000), (3, 2, 33000), (4, 2, 18000), (5, 3, 15000);
INSERT INTO orders (customer_id, total) VALUES (9, 20000);
-- FOREIGN KEY constraint failed
REFERENCES customers(id)menjadikancustomer_idforeign key ke primary keycustomers. Pelanggan nomor 9 gak ada, jadi pesanannya ditolak.ON DELETE SET NULLmenentukan apa yang terjadi pada pesanan kalau pelanggannya dihapus. Bagian berikutnya membahas pilihannya.customer_idsengaja gakNOT NULL, supayaSET NULLbisa mengosongkannya.PRAGMA foreign_keys = ON;khusus SQLite (Lab, Playground): tanpa baris ini SQLite gak memeriksa foreign key sama sekali. MySQL (InnoDB) dan PostgreSQL selalu memeriksanya.
ON DELETE: nasib baris anak
customers adalah tabel induk, orders tabel anak. Ini hasil DELETE FROM customers WHERE id = 2; (Siti, pemilik pesanan 3 dan 4) di SQLite sungguhan, untuk tiap pilihan ON DELETE:
| ON DELETE | Hasil DELETE | Pesanan 3 dan 4 |
|---|---|---|
CASCADE |
berhasil | ikut terhapus |
SET NULL |
berhasil | tetap ada, customer_id jadi NULL |
RESTRICT |
ditolak: FOREIGN KEY constraint failed |
tetap, Siti juga tetap |
NO ACTION (bawaan kalau gak ditulis) |
ditolak: FOREIGN KEY constraint failed |
tetap, Siti juga tetap |
Dengan SET NULL, pesanan Siti tetap ada tanpa pemilik. Itu jawaban tebakannya.
- CASCADE cocok kalau anak gak ada artinya tanpa induk:
order_itemsikut terhapus bersamaorders-nya, atau semua post ikut terhapus waktu user menghapus akunnya. - SET NULL cocok kalau anak harus tetap ada: author dihapus, post-nya tetap tampil tanpa author.
- RESTRICT cocok kalau penghapusan harus ditolak selama masih ada anak: department gak boleh dihapus selama masih ada employee di dalamnya.
- NO ACTION adalah bawaan SQL. Bedanya dengan
RESTRICTcuma soal kapan dicek:RESTRICTlangsung,NO ACTIONdi akhir statement (PostgreSQL bahkan bisa menundanya sampai akhir transaksi). Di MySQL keduanya sama persis. Ada jugaSET DEFAULT, yang jarang dipakai.
Untuk pesanan di toko, CASCADE dari customers hampir selalu salah: menghapus pelanggan ikut menghapus catatan penjualan dan pembukuan. Pilih RESTRICT atau SET NULL.
Di Laravel: foreignId()->constrained()
Migration Laravel menulis foreign key yang sama dengan lebih ringkas:
$table->foreignId('customer_id')->constrained(); // REFERENCES customers(id)
$table->foreignId('order_id')->constrained()->cascadeOnDelete(); // ON DELETE CASCADE
$table->foreignId('author_id')->nullable()->constrained('users')->nullOnDelete(); // ON DELETE SET NULL
$table->foreignId('department_id')->constrained()->restrictOnDelete(); // ON DELETE RESTRICT
foreignId('customer_id')membuat kolom angka yang cocok denganidauto-increment.constrained()membuat foreign key-nya dan menebak tabelnya dari nama kolom:customer_idke tabelcustomers, kolomid. Kalau namanya gak cocok, sebut tabelnya:constrained('users').cascadeOnDelete()sama denganonDelete('cascade')yang sering ketemu di tutorial lama.nullable()ditulis sebelumconstrained(), dan wajib untuknullOnDelete().
⚠️ Jebakan yang sering ketemu
- Lupa
PRAGMA foreign_keys = ONdi SQLite. Tanpa itu,ON DELETE CASCADEpun gak jalan: Siti terhapus, pesanannya tertinggal menunjuk pelanggan yang gak ada. SET NULLdi kolomNOT NULL. Menghapus Siti gagal denganNOT NULL constraint failed: orders.customer_id. Kolomnya harus bolehNULL.CASCADEdi tempat yang salah. SatuDELETEpelanggan bisa menghapus ratusan pesanan tanpa peringatan.- Primary key dari data yang bisa berubah. Email atau nomor HP sebagai primary key bikin setiap foreign key ikut berantakan saat nilainya diganti.
- Teks di latihan. Kembalikan persis
"CASCADE","SET NULL","RESTRICT", atau"NO ACTION", huruf besar semua dan dengan spasi diSET NULL.
TL;DR: Primary key = kolom unik dan gak NULL yang menunjuk satu baris dengan pasti, sebaiknya nilai yang gak pernah berubah (auto-increment, UUID, natural key, atau composite key). Foreign key = kolom yang mereferensikan primary key tabel lain, dan database menolak penunjuk ke baris yang gak ada (FOREIGN KEY constraint failed; di SQLite setelah PRAGMA foreign_keys = ON). ON DELETE menentukan nasib anak saat induk dihapus: CASCADE ikut hapus, SET NULL kosongkan, RESTRICT tolak, NO ACTION (bawaan) juga menolak. Di Laravel: $table->foreignId('customer_id')->constrained(), ditambah cascadeOnDelete(), nullOnDelete() atau restrictOnDelete().