Skema relasi dari dua chapter sebelumnya sudah penuh REFERENCES, tetapi kolom tersebut selama ini hanya catatan — SQLite tidak menegakkannya. Database tetap menerima baris yatim yang menunjuk pelanggan yang tidak ada, dan diam saja ketika induk dihapus padahal anaknya masih ada. Chapter ini mengaktifkan penegakan foreign key, membedah pesan galatnya, lalu mengatur nasib baris anak lewat aksi ON DELETE dan ON UPDATE.

Kondisi Bawaan: Penegakan Mati #

Skema contoh kita memakai dua tabel berikut. Pesanan 100 sengaja menunjuk pelanggan 99 yang tidak ada:

sql
CREATE TABLE pelanggan (
  id INTEGER PRIMARY KEY,
  nama TEXT NOT NULL
);
CREATE TABLE pesanan (
  id INTEGER PRIMARY KEY,
  tanggal TEXT NOT NULL,
  pelanggan_id INTEGER REFERENCES pelanggan(id)
);

INSERT INTO pelanggan VALUES (1, 'Budi');
INSERT INTO pesanan VALUES (100, '2025-01-05', 99);

Kedua INSERT di atas berhasil tanpa galat. Status penegakan foreign key dibaca dengan PRAGMA foreign_keys:

sql
PRAGMA foreign_keys;

Output yang Dihasilkan:

text
0

Nilai 0 berarti penegakan mati. Aktifkan dengan PRAGMA foreign_keys = ON, lalu verifikasi:

sql
PRAGMA foreign_keys = ON;
PRAGMA foreign_keys;

Output yang Dihasilkan:

text
1

Status ini hanya berlaku untuk koneksi saat ini. Koneksi baru — misalnya menjalankan sqlite3 lagi dari terminal — selalu mulai dari 0:

bash
sqlite3 toko.db "PRAGMA foreign_keys;"

Output yang Dihasilkan:

text
0

Penegakan Berjalan #

Dengan penegakan aktif, INSERT yang merujuk induk tak dikenal ditolak:

sql
INSERT INTO pesanan VALUES (101, '2025-01-06', 99);

Output yang Dihasilkan:

text
Runtime error: FOREIGN KEY constraint failed (19)

Angka 19 adalah kode galat SQLITE_CONSTRAINT. Penegakan juga menjaga arah sebaliknya — induk tidak boleh dihapus selama masih dirujuk anak:

sql
INSERT INTO pesanan VALUES (101, '2025-01-06', 1);
sql
DELETE FROM pelanggan WHERE id = 1;

Output yang Dihasilkan:

text
Runtime error: FOREIGN KEY constraint failed (19)

Anak bernilai NULL lolos dari penegakan — pelanggan_id yang NULL berarti “belum menunjuk siapa pun”, dan NULL dianggap berbeda dari referensi salah.

Memeriksa Data Lama #

Mengaktifkan pragma tidak memvalidasi data yang sudah ada. Pesanan 100 yang kita sisipkan saat penegakan mati tetap yatim di dalam database. PRAGMA foreign_key_check memindai pelanggaran seperti itu:

sql
PRAGMA foreign_key_check;

Output yang Dihasilkan:

text
pesanan|100|pelanggan|0

Empat kolomnya adalah: tabel anak, rowid baris pelanggar, tabel induk, dan index definisi foreign key. Perbaiki datanya dengan UPDATE biasa:

sql
UPDATE pesanan SET pelanggan_id = 1 WHERE id = 100;

Aksi ON DELETE dan ON UPDATE #

Tanpa aksi tambahan, SQLite memakai NO ACTION: operasi pada induk ditolak seperti pada contoh DELETE di atas. Aksi lain dituliskan pada definisi foreign key:

AksiPerilaku saat induk dihapus/diubah
NO ACTIONOperasi ditolak bila masih ada anak (bawaan)
RESTRICTSama seperti NO ACTION, tetapi diperiksa seketika
CASCADEBaris anak ikut dihapus / kolomnya ikut diubah
SET NULLKolom foreign key anak diisi NULL
SET DEFAULTKolom foreign key anak diisi nilai DEFAULT-nya

Demo CASCADE memakai pasangan kategori-artikel:

sql
CREATE TABLE kategori (
  id INTEGER PRIMARY KEY,
  nama TEXT NOT NULL
);
CREATE TABLE artikel (
  id INTEGER PRIMARY KEY,
  judul TEXT NOT NULL,
  kategori_id INTEGER REFERENCES kategori(id)
    ON DELETE CASCADE ON UPDATE CASCADE
);

INSERT INTO kategori VALUES (1, 'Database'), (2, 'Jaringan');
INSERT INTO artikel VALUES
  (10, 'Belajar SQLite', 1),
  (11, 'Normalisasi Data', 1),
  (12, 'Dasar TCP/IP', 2);

Bedah baris penting pada definisi artikel:

  1. Baris kategori_id ... REFERENCES kategori(id): kolom ini menjadi foreign key ke kolom id milik kategori; kolom induk harus PRIMARY KEY atau UNIQUE.
  2. Baris ON DELETE CASCADE: menghapus satu kategori akan menghapus semua artikel yang menunjuknya — tidak ada galat, tidak ada baris yatim.
  3. Baris ON UPDATE CASCADE: mengubah kategori.id akan menular ke kategori_id semua artikelnya.

Pembuktiannya langsung terlihat:

sql
DELETE FROM kategori WHERE id = 1;
sql
SELECT id, judul, kategori_id FROM artikel ORDER BY id;

Output yang Dihasilkan:

text
12|Dasar TCP/IP|2

Dua artikel kategori Database ikut terhapus. ON UPDATE CASCADE bekerja pada perubahan id:

sql
UPDATE kategori SET id = 9 WHERE id = 2;
sql
SELECT id, judul, kategori_id FROM artikel ORDER BY id;

Output yang Dihasilkan:

text
12|Dasar TCP/IP|9

SET NULL membiarkan anak tetap hidup tanpa induk. Kolomnya harus boleh NULL:

sql
CREATE TABLE penulis (
  id INTEGER PRIMARY KEY,
  nama TEXT NOT NULL
);
CREATE TABLE berita (
  id INTEGER PRIMARY KEY,
  judul TEXT NOT NULL,
  penulis_id INTEGER REFERENCES penulis(id) ON DELETE SET NULL
);

INSERT INTO penulis VALUES (1, 'Budi'), (2, 'Siti');
INSERT INTO berita VALUES
  (10, 'SQLite 3.50 Rilis', 1),
  (11, 'Tips Indexing', 2);
sql
DELETE FROM penulis WHERE id = 1;
sql
SELECT id, judul, penulis_id FROM berita ORDER BY id;

Output yang Dihasilkan:

text
10|SQLite 3.50 Rilis|
11|Tips Indexing|2

SET DEFAULT serupa, tetapi mengisi nilai bawaan — pastikan nilai tersebut memang ada di tabel induk:

sql
CREATE TABLE gudang (
  id INTEGER PRIMARY KEY,
  nama TEXT NOT NULL
);
CREATE TABLE barang (
  id INTEGER PRIMARY KEY,
  nama TEXT NOT NULL,
  gudang_id INTEGER DEFAULT 1 REFERENCES gudang(id) ON DELETE SET DEFAULT
);

INSERT INTO gudang VALUES (1, 'Utama'), (2, 'Cadangan');
INSERT INTO barang VALUES
  (10, 'Kopi Gayo 200g', 2),
  (11, 'Gula Aren 500g', 2);
sql
DELETE FROM gudang WHERE id = 2;
sql
SELECT id, nama, gudang_id FROM barang ORDER BY id;

Output yang Dihasilkan:

text
10|Kopi Gayo 200g|1
11|Gula Aren 500g|1

Foreign Key ke Tabel yang Sama #

Tabel boleh mereferensikan dirinya sendiri. Struktur organisasi karyawan-manager adalah contoh klasiknya:

sql
CREATE TABLE karyawan (
  id INTEGER PRIMARY KEY,
  nama TEXT NOT NULL,
  manager_id INTEGER REFERENCES karyawan(id)
);

INSERT INTO karyawan VALUES
  (1, 'Budi', NULL),
  (2, 'Siti', 1),
  (3, 'Ani', 2);
sql
SELECT k1.nama, k2.nama AS manager
FROM karyawan k1
LEFT JOIN karyawan k2 ON k1.manager_id = k2.id
ORDER BY k1.id;

Output yang Dihasilkan:

text
Budi|
Siti|Budi
Ani|Siti

Satu tabel berperan dua kali lewat alias k1 dan k2: baris karyawan dipasangkan dengan baris manajernya dari tabel yang sama. Budi tidak punya manajer sehingga kolom manager kosong, dan penegakan tetap berjalan:

sql
INSERT INTO karyawan VALUES (4, 'Rina', 99);

Output yang Dihasilkan:

text
Runtime error: FOREIGN KEY constraint failed (19)

Kenapa penegakan ini mati secara bawaan? Foreign key baru ditambahkan ke SQLite pada versi 3.6.19 (Oktober 2009), lama setelah jutaan database beroperasi tanpa penegakan. Mengubah bawaan menjadi aktif akan memutus aplikasi lama, jadi keputusannya tetap off dan setiap koneksi modern bertanggung jawab mengaktifkannya sendiri.