Pada chapter Konsep Trigger
kita memasang trigger AFTER tanpa banyak bertanya — pilihan kata AFTER terasa seperti hiasan sintaks. Padahal kata itu adalah keputusan arsitektural: kapan kode Anda berjalan, sebelum atau sesudah baris benar-benar ditulis ke tabel. Petugas gudang Toko Maju Jaya butuh keduanya — nama produk sebaiknya dirapikan sebelum disimpan, dan catatan perubahan harga sebaiknya dibuat setelah perubahan itu nyata terjadi. PostgreSQL memberi dua momen itu lewat klausa BEFORE dan AFTER, dan keduanya punya kekuatan yang tidak bisa saling menggantikan: hanya BEFORE yang bisa mengubah atau membatalkan baris, hanya AFTER yang pantas menjadi auditor. Chapter ini membuktikan kedua klaim itu lewat demo yang berjalan sungguhan.
BEFORE: Bodyguard Sebelum Pintu #
Trigger BEFORE menyala ketika baris baru sudah dibentuk di memori, tetapi belum ditulis ke tabel. Di titik ini record NEW masih bisa Anda ubah — kolom yang diubah akan menjadi nilai yang benar-benar tersimpan. Jika function mengembalikan RETURN NULL, baris itu tidak pernah ditulis sama sekali. Inilah slot yang tepat untuk normalisasi data dan validasi yang perlu menolak baris.
Seluruh demo chapter ini memakai pasangan tabel yang sama: produk sebagai tabel yang diawasi dan log_perubahan sebagai papan catat:
-- Tabel utama dan tabel log untuk demo BEFORE dan AFTER trigger
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(12,2) NOT NULL
);
CREATE TABLE log_perubahan (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kejadian text NOT NULL
);Output yang Dihasilkan:
CREATE TABLE
CREATE TABLEAturan bisnis pertama kita: kolom nama pada tabel produk harus tersimpan dalam huruf kecil, apa pun kapitalisasi yang dikirim aplikasi kasir. Trigger BEFORE INSERT berikut merapikannya tanpa memedulikan siapa penulis datanya:
-- BEFORE INSERT: rapikan nama produk menjadi huruf kecil sebelum baris ditulis
CREATE FUNCTION kecilkan_nama() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.nama := lower(NEW.nama);
RETURN NEW;
END
$$;
CREATE TRIGGER kecilkan_nama_insert
BEFORE INSERT ON produk
FOR EACH ROW EXECUTE FUNCTION kecilkan_nama();
INSERT INTO produk (id, nama, harga) VALUES
(1, 'Kopi Kapal Kapten', 18000.00),
(2, 'TEH MELATI SAPU TJUAH', 12000.00);
SELECT id, nama, harga FROM produk ORDER BY id;Output yang Dihasilkan:
CREATE FUNCTION
CREATE TRIGGER
INSERT 0 2
id | nama | harga
----+-----------------------+----------
1 | kopi kapal kapten | 18000.00
2 | teh melati sapu tjuah | 12000.00
(2 rows)Kedua baris masuk dengan kapitalisasi yang berbeda-bebas, tetapi tersimpan rata dalam huruf kecil. Bedah mekanismenya:
NEW.nama := lower(NEW.nama);— penugasan ke kolom recordNEWmengubah baris yang akan ditulis; sisi kanan membaca nilai asli dari aplikasi, sisi kiri menimpanya dengan hasillower().RETURN NEW— baris yang sudah dirapikan dikembalikan ke server untuk ditulis. Mengembalikan recordNEW(setelah atau sebelum dimodifikasi) artinya “lanjutkan, inilah baris finalnya”.
Membatalkan Operasi dengan RETURN NULL #
Kekuatan kedua milik BEFORE yang tidak dimiliki AFTER: membatalkan baris. Ketika trigger function mengembalikan NULL, server membuang operasi untuk baris itu dan melanjutkan ke baris berikutnya — tanpa error, tanpa ROLLBACK. Demo berikut menolak produk bernilai nol atau negatif sambil mencatat penolakannya ke tabel log:
-- BEFORE INSERT dengan RETURN NULL: baris ditolak diam-diam
CREATE FUNCTION tolak_harga_nol() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.harga <= 0 THEN
INSERT INTO log_perubahan (kejadian)
VALUES ('INSERT ditolak: ' || NEW.nama || ' bernilai ' || NEW.harga);
RETURN NULL;
END IF;
RETURN NEW;
END
$$;
CREATE TRIGGER tolak_harga_nol_insert
BEFORE INSERT ON produk
FOR EACH ROW EXECUTE FUNCTION tolak_harga_nol();
-- Baris tunggal yang ditolak: perhatikan tag INSERT 0 0
INSERT INTO produk (id, nama, harga) VALUES (3, 'Barang Demo', 0.00);
-- Insert banyak baris sekaligus: yang lolas masuk, yang ditolak dibuang
INSERT INTO produk (id, nama, harga) VALUES
(4, 'Gula Pasir', 14000.00),
(5, 'Barang Uji', -500.00),
(6, 'Roti Tawar', 15000.00);
SELECT id, nama, harga FROM produk ORDER BY id;
SELECT id, kejadian FROM log_perubahan ORDER BY id;Output yang Dihasilkan:
CREATE FUNCTION
CREATE TRIGGER
INSERT 0 0
INSERT 0 2
id | nama | harga
----+-----------------------+----------
1 | kopi kapal kapten | 18000.00
2 | teh melati sapu tjuah | 12000.00
4 | gula pasir | 14000.00
6 | roti tawar | 15000.00
(4 rows)
id | kejadian
----+---------------------------------------------
1 | INSERT ditolak: barang demo bernilai 0.00
2 | INSERT ditolak: barang uji bernilai -500.00
(2 rows)Ada tiga fakta penting yang tersembunyi di output itu:
INSERT 0 0pada baris tunggal — perintah “berhasil” tanpa error, tetapi angka barisnya nol: satu-satunya baris dibatalkan trigger. Perintah SQL tidak gagal; barisnya saja tidak pernah tercipta.INSERT 0 2pada insert tiga baris —Barang Ujidibuang diam-diam, dua baris sisanya masuk. Pemanggil yang tidak memeriksa row count tidak akan tahu ada baris yang hilang.- Baris log memuat nama dalam huruf kecil (
barang demo, bukanBarang Demo) — dua triggerBEFORE INSERTkini aktif, dan PostgreSQL menjalankan trigger dengan timing dan event yang sama secara alfabetis:kecilkan_nama_insertdulu, barutolak_harga_nol_insert; penolak kedua melihatNEWversi rapikan dari trigger pertama.
AFTER: Kamera Setelah Kejadian #
Trigger AFTER baru menyala setelah baris selesai ditulis — untuk UPDATE, record OLD (versi lama) dan NEW (versi baru) sama-sama tersedia utuh. Karena barisnya sudah ada, momen ini cocok untuk efek samping: mencatat audit, mengirim notifikasi, atau memperbarui tabel ringkasan. Nilai balik function diabaikan server, jadi Anda cukup mengembalikan apa pun yang lazim.
Audit perubahan harga adalah contoh kanoniknya — dan hanya AFTER yang memberi data yang jujur, karena baris lama dan baru sudah final:
-- AFTER UPDATE: audit perubahan harga, OLD dan NEW sama-sama tersedia
CREATE FUNCTION catat_ubah_harga() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO log_perubahan (kejadian)
VALUES ('harga ' || NEW.nama || ': ' || OLD.harga || ' -> ' || NEW.harga);
RETURN NEW;
END
$$;
CREATE TRIGGER catat_ubah_harga_update
AFTER UPDATE ON produk
FOR EACH ROW EXECUTE FUNCTION catat_ubah_harga();
UPDATE produk SET harga = 19000.00 WHERE id = 1;
UPDATE produk SET harga = 12500.00 WHERE id = 2;
SELECT id, kejadian FROM log_perubahan WHERE kejadian LIKE 'harga %' ORDER BY id;Output yang Dihasilkan:
CREATE FUNCTION
CREATE TRIGGER
UPDATE 1
UPDATE 1
id | kejadian
----+---------------------------------------------------
3 | harga kopi kapal kapten: 18000.00 -> 19000.00
4 | harga teh melati sapu tjuah: 12000.00 -> 12500.00
(2 rows)Setiap UPDATE meninggalkan satu baris audit berisi kedua versi harga: 18000.00 -> 19000.00 dan 12000.00 -> 12500.00. RETURN NEW di sini murni kebiasaan yang baik — pada trigger AFTER nilai baliknya diabaikan, dan RETURN NULL pun tidak akan membatalkan apa pun.
AFTER Tidak Lagi Bisa Mengubah Baris #
Apa yang terjadi kalau trigger AFTER mencoba menulis ke NEW? PL/pgSQL menerima penugasannya secara tata bahasa — NEW tetap variabel record yang sah — tetapi barisnya sudah tersimpan, sehingga perubahan itu menguap tanpa efek dan tanpa error. File berikut membuktikannya dengan trigger AFTER UPDATE yang mencoba menimpa harga menjadi 1:
-- AFTER UPDATE mencoba mengubah NEW: diterima secara tata bahasa, tanpa efek
DROP TRIGGER catat_ubah_harga_update ON produk;
CREATE FUNCTION coba_ubah_after() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.harga := 1.00;
RETURN NEW;
END
$$;
CREATE TRIGGER coba_ubah_after_update
AFTER UPDATE ON produk
FOR EACH ROW EXECUTE FUNCTION coba_ubah_after();
UPDATE produk SET harga = 22000.00 WHERE id = 1;
SELECT id, nama, harga FROM produk WHERE id = 1;Output yang Dihasilkan:
DROP TRIGGER
CREATE FUNCTION
CREATE TRIGGER
UPDATE 1
id | nama | harga
----+-------------------+----------
1 | kopi kapal kapten | 22000.00
(1 row)Perintah UPDATE meminta harga Rp22.000, trigger AFTER menimpa NEW.harga := 1.00, dan hasil akhirnya tetap 22000.00 — penugasan itu tidak pernah sampai ke tabel. Jangan andalkan error untuk menangkap kesalahan semacam ini; PostgreSQL tidak menganggapnya kesalahan. Trigger auditor lama sengaja dijatuhkan dulu di file yang sama agar output tidak bercampur baris log dari catat_ubah_harga.
Memilih Momen: BEFORE atau AFTER #
| Aspek | BEFORE | AFTER |
|---|---|---|
| Momen berjalan | sebelum baris ditulis | setelah baris ditulis |
Mengubah NEW | ya, tersimpan ke tabel | penugasan diterima, tanpa efek |
| Membatalkan baris | ya, lewat RETURN NULL | tidak — nilai balik diabaikan |
OLD pada UPDATE | tersedia | tersedia |
| Kegunaan khas | normalisasi, validasi, penolakan | audit, notifikasi, tabel ringkasan |
Kedua momen itu baru satu sumbu pilihan. Sumbu kedua yang sama menentukan bentuknya: trigger menyala sekali per baris atau sekali per perintah. Insert tiga baris sekaligus tadi membuat trigger baris kita berjalan tiga kali — bagaimana dengan trigger per pernyataan, dan variabel apa yang tersedia pada masing-masing? Chapter berikutnya, Row-Level vs Statement-Level , mengukur keduanya dengan penghitung nyata.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
after-auditor.sql
Unduh berkas-- AFTER UPDATE: audit perubahan harga, OLD dan NEW sama-sama tersedia
CREATE FUNCTION catat_ubah_harga() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
INSERT INTO log_perubahan (kejadian)
VALUES ('harga ' || NEW.nama || ': ' || OLD.harga || ' -> ' || NEW.harga);
RETURN NEW;
END
$$;
CREATE TRIGGER catat_ubah_harga_update
AFTER UPDATE ON produk
FOR EACH ROW EXECUTE FUNCTION catat_ubah_harga();
UPDATE produk SET harga = 19000.00 WHERE id = 1;
UPDATE produk SET harga = 12500.00 WHERE id = 2;
SELECT id, kejadian FROM log_perubahan WHERE kejadian LIKE 'harga %' ORDER BY id;
after-tak-bisa-ubah.sql
Unduh berkas-- AFTER UPDATE mencoba mengubah NEW: diterima secara tata bahasa, tanpa efek
DROP TRIGGER catat_ubah_harga_update ON produk;
CREATE FUNCTION coba_ubah_after() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.harga := 1.00;
RETURN NEW;
END
$$;
CREATE TRIGGER coba_ubah_after_update
AFTER UPDATE ON produk
FOR EACH ROW EXECUTE FUNCTION coba_ubah_after();
UPDATE produk SET harga = 22000.00 WHERE id = 1;
SELECT id, nama, harga FROM produk WHERE id = 1;
before-pembatal.sql
Unduh berkas-- BEFORE INSERT dengan RETURN NULL: baris ditolak diam-diam
CREATE FUNCTION tolak_harga_nol() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
IF NEW.harga <= 0 THEN
INSERT INTO log_perubahan (kejadian)
VALUES ('INSERT ditolak: ' || NEW.nama || ' bernilai ' || NEW.harga);
RETURN NULL;
END IF;
RETURN NEW;
END
$$;
CREATE TRIGGER tolak_harga_nol_insert
BEFORE INSERT ON produk
FOR EACH ROW EXECUTE FUNCTION tolak_harga_nol();
-- Baris tunggal yang ditolak: perhatikan tag INSERT 0 0
INSERT INTO produk (id, nama, harga) VALUES (3, 'Barang Demo', 0.00);
-- Insert banyak baris sekaligus: yang lolas masuk, yang ditolak dibuang
INSERT INTO produk (id, nama, harga) VALUES
(4, 'Gula Pasir', 14000.00),
(5, 'Barang Uji', -500.00),
(6, 'Roti Tawar', 15000.00);
SELECT id, nama, harga FROM produk ORDER BY id;
SELECT id, kejadian FROM log_perubahan ORDER BY id;
before-pengubah.sql
Unduh berkas-- BEFORE INSERT: rapikan nama produk menjadi huruf kecil sebelum baris ditulis
CREATE FUNCTION kecilkan_nama() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN
NEW.nama := lower(NEW.nama);
RETURN NEW;
END
$$;
CREATE TRIGGER kecilkan_nama_insert
BEFORE INSERT ON produk
FOR EACH ROW EXECUTE FUNCTION kecilkan_nama();
INSERT INTO produk (id, nama, harga) VALUES
(1, 'Kopi Kapal Kapten', 18000.00),
(2, 'TEH MELATI SAPU TJUAH', 12000.00);
SELECT id, nama, harga FROM produk ORDER BY id;
buat-tabel.sql
Unduh berkas-- Tabel utama dan tabel log untuk demo BEFORE dan AFTER trigger
CREATE TABLE produk (
id integer PRIMARY KEY,
nama text NOT NULL,
harga numeric(12,2) NOT NULL
);
CREATE TABLE log_perubahan (
id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
kejadian text NOT NULL
);