Siapa yang mencatat setiap perubahan harga? Siapa yang mengisi kolom updated_at setiap kali baris disentuh? Kita bisa memaksa aplikasi mengingat semua itu di setiap titik penulisan — dan berdoa tidak ada satu pun jalur kode yang lupa. Trigger menyelesaikannya di sisi database: engine SQLite menjalankan aksi otomatis setiap kali INSERT, UPDATE, atau DELETE menyentuh tabel tertentu, terlepas dari siapa atau apa yang memicu perubahan tersebut. Kita akan membangun audit log, penjaga updated_at, validasi bisnis, hingga view yang bisa ditulis.
Data Contoh #
Tabel utama kita adalah produk, dan tabel harga_log akan menampung jejak perubahan harga secara otomatis. Kolom diubah pada tabel log terisi lewat nilai default datetime('now'):
CREATE TABLE produk (
id INTEGER PRIMARY KEY,
nama TEXT NOT NULL,
harga REAL NOT NULL
);
INSERT INTO produk (id, nama, harga) VALUES
(1, 'Kopi Gayo', 45000),
(2, 'Teh Melati', 18000),
(3, 'Gula Aren', 25000);
CREATE TABLE harga_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
produk_id INTEGER NOT NULL,
harga_lama REAL NOT NULL,
harga_baru REAL NOT NULL,
diubah TEXT NOT NULL DEFAULT (datetime('now'))
);Trigger Pertama: Audit Log Perubahan Harga #
Trigger dideklarasikan dengan CREATE TRIGGER lalu badan program berisi statement SQL di antara BEGIN dan END:
CREATE TRIGGER log_harga AFTER UPDATE OF harga ON produk
FOR EACH ROW
WHEN NEW.harga <> OLD.harga
BEGIN
INSERT INTO harga_log (produk_id, harga_lama, harga_baru)
VALUES (NEW.id, OLD.harga, NEW.harga);
END;Mari kita bedah deklarasi tersebut baris per baris:
- Baris 1 (
AFTER UPDATE OF harga ON produk): Trigger menyala setelah kolomhargapada tabelprodukdiubah; UPDATE pada kolom lain diabaikan. KataAFTERberarti aksi berjalan setelah baris baru tersimpan. - Baris 2 (
FOR EACH ROW): Aksi berjalan sekali per baris yang tersentuh — satu-satunya mode yang didukung SQLite (tidak ada FOR EACH STATEMENT). - Baris 3 (
WHEN NEW.harga <> OLD.harga): Syarat tambahan agar trigger hanya menyala bila harga benar-benar berubah;NEWmemuat nilai baru,OLDnilai lama. - Baris 4-6 (
INSERT INTO harga_log ...): Badan trigger menyalinNEW.id,OLD.harga, danNEW.hargake tabel audit.
Mari kita buktikan trigger bekerja dengan mengubah satu harga:
UPDATE produk SET harga = 48000 WHERE id = 1;
SELECT produk_id, harga_lama, harga_baru FROM harga_log;Output yang Dihasilkan:
1|45000.0|48000.0Dua uji berikutnya memastikan trigger tidak menyala berlebihan. UPDATE yang tidak menyentuh kolom harga diabaikan, dan UPDATE yang menulis nilai sama ditolak oleh klausa WHEN:
UPDATE produk SET nama = 'Kopi Gayo Premium' WHERE id = 1;
SELECT COUNT(*) FROM harga_log;
UPDATE produk SET harga = 48000 WHERE id = 1;
SELECT COUNT(*) FROM harga_log;Output yang Dihasilkan:
1
1Menjaga Kolom updated_at Otomatis #
Trigger juga cocok memelihara kolom turunan. Kita tambahkan kolom updated_at lalu sebuah trigger yang menuliskannya setiap kali baris produk berubah:
ALTER TABLE produk ADD COLUMN updated_at TEXT;
CREATE TRIGGER set_updated_at AFTER UPDATE ON produk
FOR EACH ROW
BEGIN
UPDATE produk SET updated_at = datetime('now') WHERE id = NEW.id;
END;
UPDATE produk SET harga = 20000 WHERE id = 2;
SELECT nama, harga, updated_at FROM produk WHERE id = 2;Output yang Dihasilkan (kolom updated_at berisi waktu eksekusi contoh):
Teh Melati|20000.0|2026-10-13 04:41:08Validasi Data dengan BEFORE dan RAISE #
Timing BEFORE menjalankan aksi sebelum perubahan diterapkan — momen yang tepat untuk memveto data buruk. Fungsi RAISE(ABORT, 'pesan') membatalkan statement dan menampilkan pesan galat kita sendiri:
CREATE TRIGGER cek_harga BEFORE UPDATE ON produk
FOR EACH ROW
WHEN NEW.harga <= 0
BEGIN
SELECT RAISE(ABORT, 'harga harus bernilai positif');
END;Sekarang harga negatif tertahan di depan pintu:
sqlite3 toko.db "UPDATE produk SET harga = -5000 WHERE id = 3;"
sqlite3 toko.db "SELECT harga FROM produk WHERE id = 3;"Output yang Dihasilkan:
Error: stepping, harga harus bernilai positif (19)
25000.0Harga Gula Aren tetap 25000.0 karena statement dibatalkan sebelum menyentuh tabel.
Trigger INSTEAD OF: Menulis ke View #
View secara bawaan hanya bisa dibaca — SQLite menolak INSERT, UPDATE, dan DELETE padanya. Trigger INSTEAD OF mengganti penulisan tersebut dengan aksi yang kita definisikan, menjadikan view bisa ditulis:
CREATE VIEW produk_murah AS
SELECT id, nama, harga FROM produk WHERE harga < 30000;
SELECT * FROM produk_murah ORDER BY id;Output yang Dihasilkan:
2|Teh Melati|20000.0
3|Gula Aren|25000.0Trigger berikut meneruskan UPDATE pada view ke tabel aslinya:
CREATE TRIGGER upd_murah INSTEAD OF UPDATE ON produk_murah
BEGIN
UPDATE produk SET nama = NEW.nama, harga = NEW.harga WHERE id = OLD.id;
END;
UPDATE produk_murah SET harga = 21000 WHERE id = 2;
SELECT nama, harga FROM produk WHERE id = 2;Output yang Dihasilkan:
Teh Melati|21000.0Nilai OLD.id berasal dari baris view, lalu UPDATE mengubah baris produk yang mendasarinya.
Mengelola Trigger dan Perilakunya #
Trigger disimpan sebagai objek skema, sehingga bisa diperiksa lewat tabel sqlite_master:
SELECT name FROM sqlite_master WHERE type = 'trigger' ORDER BY name;Output yang Dihasilkan:
cek_harga
log_harga
set_updated_at
upd_murahDROP TRIGGER menghapus trigger tanpa menyentuh data. Setelah log_harga dihapus, perubahan harga tidak lagi tercatat:
DROP TRIGGER log_harga;
SELECT name FROM sqlite_master WHERE type = 'trigger' ORDER BY name;Output yang Dihasilkan:
cek_harga
set_updated_at
upd_murahBila beberapa trigger memantau event yang sama, engine SQLite menjalankannya satu per satu. Pada SQLite 3.50.1, hasil uji menunjukkan trigger yang dibuat belakangan menyala lebih dulu — urutan ini tidak dijamin spesifikasinya, jadi bila urutan langkah penting, gabungkan logikanya ke dalam satu trigger saja:
CREATE TABLE log_urutan (pesan TEXT);
CREATE TRIGGER pertama AFTER INSERT ON produk BEGIN
INSERT INTO log_urutan VALUES ('trigger pertama');
END;
CREATE TRIGGER kedua AFTER INSERT ON produk BEGIN
INSERT INTO log_urutan VALUES ('trigger kedua');
END;
INSERT INTO produk (id, nama, harga) VALUES (4, 'Sirup Vanila', 27500);
SELECT pesan FROM log_urutan;Output yang Dihasilkan:
trigger kedua
trigger pertamaTerakhir, trigger yang menulis ke tabelnya sendiri bisa saja bersarang bila kita mengaktifkan PRAGMA recursive_triggers. Nilai defaultnya adalah 0 (nonaktif):
PRAGMA recursive_triggers;Output yang Dihasilkan:
0Trigger gema berikut menyalin isi baru ke baris tambahan selama teksnya masih pendek. Dengan recursive_triggers nonaktif, INSERT di dalam badan trigger tidak memicu trigger lagi:
CREATE TABLE catatan (isi TEXT);
CREATE TRIGGER gema AFTER INSERT ON catatan
WHEN length(NEW.isi) < 10
BEGIN
INSERT INTO catatan VALUES ('gema ' || NEW.isi);
END;
INSERT INTO catatan VALUES ('ho');
SELECT * FROM catatan;Output yang Dihasilkan:
ho
gema hoAktifkan PRAGMA dan uji ulang — gema kini bergema:
PRAGMA recursive_triggers = 1;
INSERT INTO catatan VALUES ('hei');
SELECT * FROM catatan;Output yang Dihasilkan:
ho
gema ho
hei
gema hei
gema gema heiTrigger mengotomatiskan reaksi atas perubahan data. Fitur lanjutan terakhir di section ini mencari teks dalam jumlah dokumen besar: Full-Text Search dengan FTS5.