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:
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:
PRAGMA foreign_keys;Output yang Dihasilkan:
0Nilai 0 berarti penegakan mati. Aktifkan dengan PRAGMA foreign_keys = ON, lalu verifikasi:
PRAGMA foreign_keys = ON;
PRAGMA foreign_keys;Output yang Dihasilkan:
1Status ini hanya berlaku untuk koneksi saat ini. Koneksi baru — misalnya menjalankan sqlite3 lagi dari terminal — selalu mulai dari 0:
sqlite3 toko.db "PRAGMA foreign_keys;"Output yang Dihasilkan:
0Penegakan Berjalan #
Dengan penegakan aktif, INSERT yang merujuk induk tak dikenal ditolak:
INSERT INTO pesanan VALUES (101, '2025-01-06', 99);Output yang Dihasilkan:
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:
INSERT INTO pesanan VALUES (101, '2025-01-06', 1);DELETE FROM pelanggan WHERE id = 1;Output yang Dihasilkan:
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:
PRAGMA foreign_key_check;Output yang Dihasilkan:
pesanan|100|pelanggan|0Empat kolomnya adalah: tabel anak, rowid baris pelanggar, tabel induk, dan index definisi foreign key. Perbaiki datanya dengan UPDATE biasa:
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:
| Aksi | Perilaku saat induk dihapus/diubah |
|---|---|
NO ACTION | Operasi ditolak bila masih ada anak (bawaan) |
RESTRICT | Sama seperti NO ACTION, tetapi diperiksa seketika |
CASCADE | Baris anak ikut dihapus / kolomnya ikut diubah |
SET NULL | Kolom foreign key anak diisi NULL |
SET DEFAULT | Kolom foreign key anak diisi nilai DEFAULT-nya |
Demo CASCADE memakai pasangan kategori-artikel:
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:
- Baris
kategori_id ... REFERENCES kategori(id): kolom ini menjadi foreign key ke kolomidmilikkategori; kolom induk harus PRIMARY KEY atau UNIQUE. - Baris
ON DELETE CASCADE: menghapus satu kategori akan menghapus semua artikel yang menunjuknya — tidak ada galat, tidak ada baris yatim. - Baris
ON UPDATE CASCADE: mengubahkategori.idakan menular kekategori_idsemua artikelnya.
Pembuktiannya langsung terlihat:
DELETE FROM kategori WHERE id = 1;SELECT id, judul, kategori_id FROM artikel ORDER BY id;Output yang Dihasilkan:
12|Dasar TCP/IP|2Dua artikel kategori Database ikut terhapus. ON UPDATE CASCADE bekerja pada perubahan id:
UPDATE kategori SET id = 9 WHERE id = 2;SELECT id, judul, kategori_id FROM artikel ORDER BY id;Output yang Dihasilkan:
12|Dasar TCP/IP|9SET NULL membiarkan anak tetap hidup tanpa induk. Kolomnya harus boleh NULL:
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);DELETE FROM penulis WHERE id = 1;SELECT id, judul, penulis_id FROM berita ORDER BY id;Output yang Dihasilkan:
10|SQLite 3.50 Rilis|
11|Tips Indexing|2SET DEFAULT serupa, tetapi mengisi nilai bawaan — pastikan nilai tersebut memang ada di tabel induk:
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);DELETE FROM gudang WHERE id = 2;SELECT id, nama, gudang_id FROM barang ORDER BY id;Output yang Dihasilkan:
10|Kopi Gayo 200g|1
11|Gula Aren 500g|1Foreign Key ke Tabel yang Sama #
Tabel boleh mereferensikan dirinya sendiri. Struktur organisasi karyawan-manager adalah contoh klasiknya:
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);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:
Budi|
Siti|Budi
Ani|SitiSatu 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:
INSERT INTO karyawan VALUES (4, 'Rina', 99);Output yang Dihasilkan:
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.