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'):

sql
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:

sql
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:

  1. Baris 1 (AFTER UPDATE OF harga ON produk): Trigger menyala setelah kolom harga pada tabel produk diubah; UPDATE pada kolom lain diabaikan. Kata AFTER berarti aksi berjalan setelah baris baru tersimpan.
  2. Baris 2 (FOR EACH ROW): Aksi berjalan sekali per baris yang tersentuh — satu-satunya mode yang didukung SQLite (tidak ada FOR EACH STATEMENT).
  3. Baris 3 (WHEN NEW.harga <> OLD.harga): Syarat tambahan agar trigger hanya menyala bila harga benar-benar berubah; NEW memuat nilai baru, OLD nilai lama.
  4. Baris 4-6 (INSERT INTO harga_log ...): Badan trigger menyalin NEW.id, OLD.harga, dan NEW.harga ke tabel audit.

Mari kita buktikan trigger bekerja dengan mengubah satu harga:

sql
UPDATE produk SET harga = 48000 WHERE id = 1;
SELECT produk_id, harga_lama, harga_baru FROM harga_log;

Output yang Dihasilkan:

text
1|45000.0|48000.0

Dua uji berikutnya memastikan trigger tidak menyala berlebihan. UPDATE yang tidak menyentuh kolom harga diabaikan, dan UPDATE yang menulis nilai sama ditolak oleh klausa WHEN:

sql
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:

text
1
1

Menjaga 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:

sql
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):

text
Teh Melati|20000.0|2026-10-13 04:41:08

Validasi 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:

sql
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:

bash
sqlite3 toko.db "UPDATE produk SET harga = -5000 WHERE id = 3;"
sqlite3 toko.db "SELECT harga FROM produk WHERE id = 3;"

Output yang Dihasilkan:

text
Error: stepping, harga harus bernilai positif (19)
25000.0

Harga 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:

sql
CREATE VIEW produk_murah AS
  SELECT id, nama, harga FROM produk WHERE harga < 30000;

SELECT * FROM produk_murah ORDER BY id;

Output yang Dihasilkan:

text
2|Teh Melati|20000.0
3|Gula Aren|25000.0

Trigger berikut meneruskan UPDATE pada view ke tabel aslinya:

sql
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:

text
Teh Melati|21000.0

Nilai 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:

sql
SELECT name FROM sqlite_master WHERE type = 'trigger' ORDER BY name;

Output yang Dihasilkan:

text
cek_harga
log_harga
set_updated_at
upd_murah

DROP TRIGGER menghapus trigger tanpa menyentuh data. Setelah log_harga dihapus, perubahan harga tidak lagi tercatat:

sql
DROP TRIGGER log_harga;
SELECT name FROM sqlite_master WHERE type = 'trigger' ORDER BY name;

Output yang Dihasilkan:

text
cek_harga
set_updated_at
upd_murah

Bila 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:

sql
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:

text
trigger kedua
trigger pertama

Terakhir, trigger yang menulis ke tabelnya sendiri bisa saja bersarang bila kita mengaktifkan PRAGMA recursive_triggers. Nilai defaultnya adalah 0 (nonaktif):

sql
PRAGMA recursive_triggers;

Output yang Dihasilkan:

text
0

Trigger 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:

sql
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:

text
ho
gema ho

Aktifkan PRAGMA dan uji ulang — gema kini bergema:

sql
PRAGMA recursive_triggers = 1;
INSERT INTO catatan VALUES ('hei');
SELECT * FROM catatan;

Output yang Dihasilkan:

text
ho
gema ho
hei
gema hei
gema gema hei

Trigger mengotomatiskan reaksi atas perubahan data. Fitur lanjutan terakhir di section ini mencari teks dalam jumlah dokumen besar: Full-Text Search dengan FTS5.