Trigger yang kita pasang sejauh ini selalu menempel pada satu peristiwa, biasanya INSERT. Padahal tabel produk Toko Maju Jaya hidup dalam tiga irama sekaligus: barang masuk (INSERT), harga dan stok berubah (UPDATE), barang ditarik dari etalase (DELETE). PostgreSQL membiarkan Anda memilih peristiwa mana yang didengarkan — satu saja, kombinasi INSERT OR UPDATE OR DELETE, atau bahkan “kolom harga saja yang di-UPDATE”. Pilihan itu bukan gaya-gayaan: peristiwa menentukan record mana yang tersedia, dan menyentuh record yang tidak ada di peristiwa itu adalah sumber bug paling umum di dunia trigger. Chapter ini memetakan ketiga peristiwa, membongkar jebakan NEW pada DELETE, lalu menutup dengan peristiwa keempat yang sering dilupakan: TRUNCATE.
Seluruh demo memakai tabel produk dengan kolom stok tambahan — supaya UPDATE punya dua kolom yang bisa dibedakan — dan tabel log_kejadian sebagai papan catat:
-- Tabel produk dan log kejadian untuk demo peristiwa INSERT UPDATE DELETE
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(12,2) NOT NULL,
stok integer NOT NULL DEFAULT 0
);
CREATE TABLE log_kejadian (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kejadian text NOT NULL
);Output yang Dihasilkan:
CREATE TABLE
CREATE TABLESatu Trigger untuk Tiga Peristiwa #
Daftar peristiwa di CREATE TRIGGER bisa memuat satu event atau beberapa sekaligus dengan pemisah OR. Satu function yang sama lalu menerima ketiga jenis panggilan, dan variabel TG_OP menjadi saklar pemilih cabang. Ini pola standar untuk pencatat kejadian yang ingin memperlakukan ketiga peristiwa dengan logika berbeda:
-- Satu trigger untuk INSERT OR UPDATE OR DELETE, bercabang lewat TG_OP
CREATE FUNCTION catat_peristiwa() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO log_kejadian (kejadian)
VALUES ('INSERT: masuk ' || NEW.nama);
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO log_kejadian (kejadian)
VALUES ('UPDATE: ' || NEW.nama || ' harga ' || OLD.harga || ' -> ' || NEW.harga
|| ', stok ' || OLD.stok || ' -> ' || NEW.stok);
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
INSERT INTO log_kejadian (kejadian)
VALUES ('DELETE: keluar ' || OLD.nama);
RETURN OLD;
END IF;
RETURN NULL;
END
$$;
CREATE TRIGGER catat_peristiwa_produk
AFTER INSERT OR UPDATE OR DELETE ON produk
FOR EACH ROW EXECUTE FUNCTION catat_peristiwa();
INSERT INTO produk (id, nama, harga, stok) VALUES (1, 'Kopi Kapal', 18000.00, 10);
UPDATE produk SET harga = 19000.00 WHERE id = 1;
DELETE FROM produk WHERE id = 1;
SELECT id, kejadian FROM log_kejadian ORDER BY id;Output yang Dihasilkan:
CREATE FUNCTION
CREATE TRIGGER
INSERT 0 1
UPDATE 1
DELETE 1
id | kejadian
----+--------------------------------------------------------------
1 | INSERT: masuk Kopi Kapal
2 | UPDATE: Kopi Kapal harga 18000.00 -> 19000.00, stok 10 -> 10
3 | DELETE: keluar Kopi Kapal
(3 rows)Tiga perintah, tiga baris log dengan tiga watak berbeda. Perhatikan siapa yang bekerja di tiap cabang:
- Cabang
TG_OP = 'INSERT'hanya membacaNEW— baris lama memang tidak ada pada INSERT. - Cabang
TG_OP = 'UPDATE'memakai keduanya:OLD.hargadanNEW.hargauntuk harga,OLD.stokdanNEW.stokuntuk stok — sisi kiri->selalu versi lama, sisi kanan versi baru. - Cabang
TG_OP = 'DELETE'hanya membacaOLD— baris baru tidak ada pada DELETE — dan mengembalikanRETURN OLDagar penghapusan berjalan. - Nilai balik tiap cabang mengikuti semantik dari chapter Trigger Function
:
RETURN NEWuntuk INSERT/UPDATE,RETURN OLDuntuk DELETE.
Record yang Tersedia per Peristiwa #
| Peristiwa | NEW | OLD | Keterangan |
|---|---|---|---|
INSERT | baris yang akan masuk | NULL | tidak ada versi lama |
UPDATE | baris hasil perubahan | baris sebelum diubah | satu-satunya peristiwa dua record |
DELETE | NULL | baris yang akan dihapus | tidak ada versi baru |
TRUNCATE | NULL | NULL | hanya statement-level |
Tabel ini adalah kontrak yang dipegang server — dan justru di kolom NULL itulah bahaya bersembunyi.
Jebakan NEW pada Trigger DELETE #
Apa yang terjadi bila function trigger DELETE iseng membaca NEW.nama? Jawabannya mengejutkan pemula: tidak ada error. NEW pada peristiwa DELETE bernilai NULL, dan membaca kolom dari record NULL menghasilkan NULL yang mengalir diam-diam ke dalam query Anda. Bukti langsungnya:
-- Membaca NEW di dalam trigger DELETE: nilainya NULL, bukan error
CREATE FUNCTION periksa_new_delete() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO log_kejadian (kejadian)
VALUES ('NEW saat DELETE: nama = ' || coalesce(NEW.nama, '<NULL>')
|| ', NEW IS NULL = ' || (NEW IS NULL));
RETURN OLD;
END
$$;
CREATE TRIGGER periksa_new_delete_barang
BEFORE DELETE ON produk
FOR EACH ROW EXECUTE FUNCTION periksa_new_delete();
INSERT INTO produk (id, nama, harga, stok) VALUES (2, 'Teh Melati', 12000.00, 5);
DELETE FROM produk WHERE id = 2;
SELECT id, kejadian FROM log_kejadian WHERE kejadian LIKE 'NEW %' ORDER BY id;Output yang Dihasilkan:
CREATE FUNCTION
CREATE TRIGGER
INSERT 0 1
DELETE 1
id | kejadian
----+----------------------------------------------------
5 | NEW saat DELETE: nama = <NULL>, NEW IS NULL = true
(1 row)Baris log membuktikan dua hal sekaligus: NEW.nama bernilai NULL (digantikan <NULL> oleh coalesce()) dan uji NEW IS NULL mengembalikan true. Trigger DELETE tetap berjalan normal dan RETURN OLD tetap menghapus baris — bug semacam ini tidak pernah menghentikan apa pun, ia hanya menghasilkan data kosong di tempat yang Anda kira berisi data.
Kesalahan baru ditolak keras server kalau NEW dirujuk di tempat yang strukturnya diperiksa saat pendaftaran — klausa WHEN pada trigger DELETE:
-- WHEN pada trigger DELETE tidak boleh menyentuh NEW
CREATE TRIGGER harga_delete_terlarang
BEFORE DELETE ON produk
FOR EACH ROW
WHEN (NEW.harga > 0)
EXECUTE FUNCTION periksa_new_delete();Output yang Dihasilkan:
psql:new-saat-delete-gagal.sql:6: ERROR: DELETE trigger's WHEN condition cannot reference NEW values
LINE 4: WHEN (NEW.harga > 0)
^Error DELETE trigger's WHEN condition cannot reference NEW values muncul saat CREATE TRIGGER dijalankan, sebelum satu baris data pun tersentuh — pemeriksaan statis ini menyelamatkan Anda dari satu kelas bug, tetapi hanya di klausa WHEN; badan function tetap sepenuhnya tanggung jawab Anda.
UPDATE OF: Menyala Hanya untuk Kolom Tertentu #
Tabel produk diubah setiap hari — harga naik-turun, stok berkurang, nama dirapikan. Kalau aturannya hanya tentang harga, memicu function pada setiap UPDATE apa pun adalah pemborosan. Bentuk UPDATE OF nama_kolom mempersempit peristiwa ke kolom yang Anda sebutkan:
-- UPDATE OF harga: hanya perubahan kolom harga yang memicu trigger
CREATE TABLE log_harga_target (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
produk_id integer NOT NULL,
harga_lama numeric(12,2) NOT NULL,
harga_baru numeric(12,2) NOT NULL
);
CREATE FUNCTION catat_harga_target() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO log_harga_target (produk_id, harga_lama, harga_baru)
VALUES (NEW.id, OLD.harga, NEW.harga);
RETURN NEW;
END
$$;
CREATE TRIGGER catat_harga_target_update
AFTER UPDATE OF harga ON produk
FOR EACH ROW EXECUTE FUNCTION catat_harga_target();
INSERT INTO produk (id, nama, harga, stok) VALUES (3, 'Gula Pasir', 14000.00, 20);
-- UPDATE kolom harga: trigger menyala
UPDATE produk SET harga = 14500.00 WHERE id = 3;
-- UPDATE kolom stok saja: trigger diam
UPDATE produk SET stok = 25 WHERE id = 3;
SELECT produk_id, harga_lama, harga_baru FROM log_harga_target ORDER BY id;
SELECT id, harga, stok FROM produk WHERE id = 3;Output yang Dihasilkan:
CREATE TABLE
CREATE FUNCTION
CREATE TRIGGER
INSERT 0 1
UPDATE 1
UPDATE 1
produk_id | harga_lama | harga_baru
-----------+------------+------------
3 | 14000.00 | 14500.00
(1 row)
id | harga | stok
----+----------+------
3 | 14500.00 | 25
(1 row)Dua perintah UPDATE 1 berjalan sukses, tetapi log_harga_target hanya berisi satu baris — perubahan harga dari 14000.00 ke 14500.00. UPDATE kedua yang menyentuh stok saja tidak memicu trigger ini, meski barisnya benar-benar berubah (stok 20 → 25 pada SELECT terakhir). Trigger catat_peristiwa_produk dari bagian awal chapter tetap menyala untuk keduanya — daftar kolom mempersempit satu trigger, bukan semua trigger di tabel.
TRUNCATE: Peristiwa Keempat #
Perintah TRUNCATE mengosongkan tabel sekaligus tanpa memproses baris satu per satu. Konsekuensinya dua arah: trigger DELETE sama sekali tidak menyala, dan peristiwa TRUNCATE sendiri hanya tersedia untuk statement-level — tidak ada FOR EACH ROW untuk peristiwa ini, dan NEW/OLD tidak berarti apa pun karena tidak ada baris yang diproses:
-- TRUNCATE: peristiwa statement-level yang tidak membawa NEW atau OLD
CREATE FUNCTION catat_truncate() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO log_kejadian (kejadian)
VALUES ('TRUNCATE: seluruh isi tabel dikosongkan');
RETURN NULL;
END
$$;
CREATE TRIGGER catat_truncate_produk
AFTER TRUNCATE ON produk
FOR EACH STATEMENT EXECUTE FUNCTION catat_truncate();
TRUNCATE produk;
SELECT id, kejadian FROM log_kejadian WHERE kejadian LIKE 'TRUNCATE%' ORDER BY id;
SELECT count(*) AS sisa_produk FROM produk;Output yang Dihasilkan:
CREATE FUNCTION
CREATE TRIGGER
TRUNCATE TABLE
id | kejadian
----+-----------------------------------------
10 | TRUNCATE: seluruh isi tabel dikosongkan
(1 row)
sisa_produk
-------------
0
(1 row)Tabel berubah kosong (sisa_produk = 0) dalam satu tag TRUNCATE TABLE, dan satu-satunya jejaknya adalah baris log dari trigger statement-level tadi — Gula Pasir keluar tanpa pernah menyentuh cabang TG_OP = 'DELETE'. Bila audit Anda wajib mencatat setiap baris yang hilang, kenyataan ini membuat TRUNCATE berbahaya di tabel ter-audit; ada baiknya hak TRUNCATE dibatasi terpisah dari hak DELETE.
Dengan ini rangkaian pemicu data selesai dipetakan: momen (BEFORE/AFTER), cakupan (row/statement), isi function (NEW/OLD/TG_*), dan peristiwa (INSERT/UPDATE/DELETE/TRUNCATE). Namun semua trigger yang kita tulis memvalidasi satu baris pada satu waktu dan langsung menolaknya. Bagaimana dengan aturan yang harus memeriksa keadaan lintas baris — misalnya total stok lintas gudang — dan baru boleh menolak di akhir transaksi? Untuk itu PostgreSQL punya varian khusus bernama constraint trigger, pokok bahasan chapter berikutnya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- Tabel produk dan log kejadian untuk demo peristiwa INSERT UPDATE DELETE
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(12,2) NOT NULL,
stok integer NOT NULL DEFAULT 0
);
CREATE TABLE log_kejadian (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kejadian text NOT NULL
);
new-saat-delete-gagal.sql
Unduh berkas-- WHEN pada trigger DELETE tidak boleh menyentuh NEW
CREATE TRIGGER harga_delete_terlarang
BEFORE DELETE ON produk
FOR EACH ROW
WHEN (NEW.harga > 0)
EXECUTE FUNCTION periksa_new_delete();
new-saat-delete.sql
Unduh berkas-- Membaca NEW di dalam trigger DELETE: nilainya NULL, bukan error
CREATE FUNCTION periksa_new_delete() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO log_kejadian (kejadian)
VALUES ('NEW saat DELETE: nama = ' || coalesce(NEW.nama, '<NULL>')
|| ', NEW IS NULL = ' || (NEW IS NULL));
RETURN OLD;
END
$$;
CREATE TRIGGER periksa_new_delete_barang
BEFORE DELETE ON produk
FOR EACH ROW EXECUTE FUNCTION periksa_new_delete();
INSERT INTO produk (id, nama, harga, stok) VALUES (2, 'Teh Melati', 12000.00, 5);
DELETE FROM produk WHERE id = 2;
SELECT id, kejadian FROM log_kejadian WHERE kejadian LIKE 'NEW %' ORDER BY id;
tiga-peristiwa.sql
Unduh berkas-- Satu trigger untuk INSERT OR UPDATE OR DELETE, bercabang lewat TG_OP
CREATE FUNCTION catat_peristiwa() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO log_kejadian (kejadian)
VALUES ('INSERT: masuk ' || NEW.nama);
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO log_kejadian (kejadian)
VALUES ('UPDATE: ' || NEW.nama || ' harga ' || OLD.harga || ' -> ' || NEW.harga
|| ', stok ' || OLD.stok || ' -> ' || NEW.stok);
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
INSERT INTO log_kejadian (kejadian)
VALUES ('DELETE: keluar ' || OLD.nama);
RETURN OLD;
END IF;
RETURN NULL;
END
$$;
CREATE TRIGGER catat_peristiwa_produk
AFTER INSERT OR UPDATE OR DELETE ON produk
FOR EACH ROW EXECUTE FUNCTION catat_peristiwa();
INSERT INTO produk (id, nama, harga, stok) VALUES (1, 'Kopi Kapal', 18000.00, 10);
UPDATE produk SET harga = 19000.00 WHERE id = 1;
DELETE FROM produk WHERE id = 1;
SELECT id, kejadian FROM log_kejadian ORDER BY id;
truncate-trigger.sql
Unduh berkas-- TRUNCATE: peristiwa statement-level yang tidak membawa NEW atau OLD
CREATE FUNCTION catat_truncate() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO log_kejadian (kejadian)
VALUES ('TRUNCATE: seluruh isi tabel dikosongkan');
RETURN NULL;
END
$$;
CREATE TRIGGER catat_truncate_produk
AFTER TRUNCATE ON produk
FOR EACH STATEMENT EXECUTE FUNCTION catat_truncate();
TRUNCATE produk;
SELECT id, kejadian FROM log_kejadian WHERE kejadian LIKE 'TRUNCATE%' ORDER BY id;
SELECT count(*) AS sisa_produk FROM produk;
update-of-kolom.sql
Unduh berkas-- UPDATE OF harga: hanya perubahan kolom harga yang memicu trigger
CREATE TABLE log_harga_target (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
produk_id integer NOT NULL,
harga_lama numeric(12,2) NOT NULL,
harga_baru numeric(12,2) NOT NULL
);
CREATE FUNCTION catat_harga_target() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO log_harga_target (produk_id, harga_lama, harga_baru)
VALUES (NEW.id, OLD.harga, NEW.harga);
RETURN NEW;
END
$$;
CREATE TRIGGER catat_harga_target_update
AFTER UPDATE OF harga ON produk
FOR EACH ROW EXECUTE FUNCTION catat_harga_target();
INSERT INTO produk (id, nama, harga, stok) VALUES (3, 'Gula Pasir', 14000.00, 20);
-- UPDATE kolom harga: trigger menyala
UPDATE produk SET harga = 14500.00 WHERE id = 3;
-- UPDATE kolom stok saja: trigger diam
UPDATE produk SET stok = 25 WHERE id = 3;
SELECT produk_id, harga_lama, harga_baru FROM log_harga_target ORDER BY id;
SELECT id, harga, stok FROM produk WHERE id = 3;