Sejauh ini seluruh trigger yang kita bahas bereaksi terhadap baris data dan langsung dijalankan pada saat pernyataan dieksekusi. Padahal ada satu kelompok aturan bisnis yang canggung ditangani baik oleh constraint maupun trigger biasa: aturan yang melibatkan kumpulan baris. CHECK hanya boleh membaca barisnya sendiri — subquery di dalam CHECK ditolak server, sebagaimana sudah dibuktikan di chapter Exclusion Constraints. EXCLUDE menangani relasi berpasangan antar baris lewat operator index, tetapi aturan seperti “total saldo satu kelompok maksimal Rp5.000.000” bukan relasi pasangan; aturan tersebut menuntut SUM atas seluruh anggota kelompok. Trigger biasa bisa menjalankan query apa pun, namun trigger biasa menolak baris seketika — di tengah transaksi yang mungkin masih akan memperbaiki dirinya beberapa pernyataan kemudian. PostgreSQL menyediakan jalan tengahnya: CREATE CONSTRAINT TRIGGER, yaitu trigger yang mengikuti disiplin constraint — punya nama, boleh DEFERRABLE, dan pemeriksaannya bisa digeser hingga momen COMMIT.
Trigger dengan Disiplin Constraint #
Secara mekanisme, constraint trigger adalah trigger AFTER level baris yang diperlakukan server sebagai constraint bernama. Syaratnya ketat: deklarasi hanya menerima AFTER dan FOR EACH ROW — bentuk BEFORE atau FOR EACH STATEMENT ditolak parser sejak awal. Sebagai gantinya, constraint trigger menerima klausa DEFERRABLE, sesuatu yang tidak dimiliki trigger biasa, sehingga trigger tersebut bisa ikut serta dalam mekanisme penundaan pemeriksaan yang juga dipakai foreign key.
Kita pakai satu studi kasus yang konsisten di seluruh chapter: tabel rekening anggota koperasi. Aturan bisnisnya menyatakan total saldo satu kelompok tidak boleh melebihi Rp5.000.000 — aturan agregat lintas baris yang mustahil ditulis sebagai CHECK.
-- Batas bisnis: total saldo satu kelompok maksimal Rp5.000.000
CREATE TABLE rekening (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
pemilik text NOT NULL,
kelompok text NOT NULL,
saldo numeric(12,2) NOT NULL DEFAULT 0
);
CREATE FUNCTION cek_total_saldo() RETURNS trigger AS $$
DECLARE
total numeric;
BEGIN
SELECT sum(saldo) INTO total
FROM rekening
WHERE kelompok = NEW.kelompok;
IF total > 5000000 THEN
RAISE EXCEPTION 'total saldo kelompok "%" melebihi batas %',
NEW.kelompok, 5000000;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Constraint trigger: DEFERRABLE dan terdaftar dengan nama constraint
CREATE CONSTRAINT TRIGGER cek_saldo_kelompok
AFTER INSERT OR UPDATE ON rekening
DEFERRABLE INITIALLY IMMEDIATE
FOR EACH ROW EXECUTE FUNCTION cek_total_saldo();
INSERT INTO rekening (pemilik, kelompok, saldo) VALUES
('Budi', 'keluarga-santoso', 3000000),
('Siti', 'keluarga-santoso', 1500000),
('Dewi', 'keluarga-wijaya', 1000000);
SELECT id, pemilik, kelompok, saldo FROM rekening ORDER BY id;
\d rekeningOutput yang Dihasilkan:
CREATE TABLE
CREATE FUNCTION
CREATE TRIGGER
INSERT 0 3
id | pemilik | kelompok | saldo
----+---------+------------------+------------
1 | Budi | keluarga-santoso | 3000000.00
2 | Siti | keluarga-santoso | 1500000.00
3 | Dewi | keluarga-wijaya | 1000000.00
(3 rows)
Table "public.rekening"
Column | Type | Collation | Nullable | Default
----------+---------------+-----------+----------+------------------------------
id | integer | | not null | generated always as identity
pemilik | text | | not null |
kelompok | text | | not null |
saldo | numeric(12,2) | | not null | 0
Indexes:
"rekening_pkey" PRIMARY KEY, btree (id)
Triggers:
cek_saldo_kelompok AFTER INSERT OR UPDATE ON rekening DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE FUNCTION cek_total_saldo()Mari kita bedah bagian-bagian pentingnya:
- Fungsi
cek_total_saldo(): fungsi trigger PL/pgSQL biasa dari chapter sebelumnya. Fungsi tersebut menghitungsum(saldo)seluruh baris padakelompokmilikNEW— inilah kekuatan yang tidak dimilikiCHECK, yaitu membaca baris lain. Nilaitotaldibandingkan dengan batas 5.000.000, dan pelanggaran diangkat lewatRAISE EXCEPTION. CREATE CONSTRAINT TRIGGER cek_saldo_kelompok: kata kunciCONSTRAINTmengubah status trigger menjadi constraint bernama. Namacek_saldo_kelompokbukan sekadar label — nama tersebut dipakai perintahSET CONSTRAINTSuntuk menunda pemeriksaan secara selektif.AFTER INSERT OR UPDATE ... FOR EACH ROW: satu-satunya bentuk yang diizinkan. Constraint trigger selalu diperiksa setelah baris ditulis, per baris.DEFERRABLE INITIALLY IMMEDIATE: trigger boleh ditunda (DEFERRABLE), tetapi perilaku bawaannya tetap memeriksa seketika (INITIALLY IMMEDIATE). TigaINSERTpertama pun langsung diperiksa satu per satu dan lolos karena totalkeluarga-santososaat itu 4.500.000.\d rekening: psql menampilkan constraint trigger pada bagianTriggers:beserta statusDEFERRABLE INITIALLY IMMEDIATE— keberadaannya transparan di catalog, bukan logika tersembunyi.
Pelanggaran pada Saat Pernyataan #
Dengan INITIALLY IMMEDIATE, constraint trigger berperilaku seperti trigger biasa yang cerewet: begitu total kelompok terlewati, pernyataan yang sedang berjalan langsung dibatalkan. Kita tambahkan rekening baru senilai Rp1.000.000 untuk keluarga-santoso sehingga totalnya melonjak menjadi Rp5.500.000:
-- Total keluarga-santoso akan menjadi 5.500.000: langsung ditolak
INSERT INTO rekening (pemilik, kelompok, saldo)
VALUES ('Ani', 'keluarga-santoso', 1000000);Output yang Dihasilkan:
psql:pelanggaran-immediate.sql:3: ERROR: total saldo kelompok "keluarga-santoso" melebihi batas 5000000
CONTEXT: PL/pgSQL function cek_total_saldo() line 10 at RAISEPesan error tersebut muncul dari RAISE EXCEPTION di dalam fungsi trigger, bukan dari mesin constraint. Inilah ciri pembeda yang berguna saat membaca log: error constraint trigger selalu membawa baris CONTEXT: yang menunjuk ke fungsi PL/pgSQL. Perhatikan juga bahwa id rekening Ani tidak pernah tersimpan, tetapi nilai sequence-nya sudah terpakai — baris berikutnya akan melompat ke id 5. Kesenjangan penomoran seperti ini normal pada kolom identity setelah pernyataan gagal.
Menunda Pemeriksaan hingga COMMIT #
Sekarang bagian yang tidak bisa dilakukan CHECK maupun trigger biasa: menggeser momen pemeriksaan ke akhir transaksi. Kita ganti trigger yang lama dengan versi INITIALLY DEFERRED, lalu jalankan satu transaksi yang kelebihan saldo di tengah jalan tetapi sadar diri sebelum COMMIT:
DROP TRIGGER cek_saldo_kelompok ON rekening;
-- Versi tertunda: pemeriksaan digeser ke momen COMMIT
CREATE CONSTRAINT TRIGGER cek_saldo_kelompok
AFTER INSERT OR UPDATE ON rekening
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE FUNCTION cek_total_saldo();
BEGIN;
-- Total sementara 5.100.000 (melewati batas): pernyataan diterima, belum diperiksa
INSERT INTO rekening (pemilik, kelompok, saldo)
VALUES ('Ani', 'keluarga-santoso', 600000);
-- Kompensasi: turunkan saldo Budi sehingga total kembali 4.500.000
UPDATE rekening SET saldo = 2400000 WHERE id = 1;
-- Di dalam transaksi, keadaan yang "ilegal" itu benar-benar terlihat
SELECT id, pemilik, saldo FROM rekening ORDER BY id;
COMMIT;
-- Setelah COMMIT, pemeriksaan sudah dijalankan dan lolos
SELECT id, pemilik, saldo FROM rekening ORDER BY id;Output yang Dihasilkan:
DROP TRIGGER
CREATE TRIGGER
BEGIN
INSERT 0 1
UPDATE 1
id | pemilik | saldo
----+---------+------------
1 | Budi | 2400000.00
2 | Siti | 1500000.00
3 | Dewi | 1000000.00
5 | Ani | 600000.00
(4 rows)
COMMIT
id | pemilik | saldo
----+---------+------------
1 | Budi | 2400000.00
2 | Siti | 1500000.00
3 | Dewi | 1000000.00
5 | Ani | 600000.00
(4 rows)Peristiwa di dalam transaksi tersebut berjalan seperti ini:
INSERTrekening Ani Rp600.000: totalkeluarga-santosomenjadi Rp5.100.000 — melewati batas. Karena trigger bersifatINITIALLY DEFERRED, server tidak menjalankan fungsi pemeriksaan saat itu juga; server hanya mencatat pemanggilan tertunda pada antrean transaksi.UPDATEsaldo Budi menjadi Rp2.400.000: total kembali turun ke Rp4.500.000. Pemanggilan tertunda kedua ikut masuk antrean untuk baris Budi.SELECTdi dalam transaksi: konsisten membaca keadaan sementara — kelebihan saldo tadi pernah nyata terjadi, dan pembaca di dalam transaksi yang sama bisa melihatnya. Trigger tertunda tidak menyensor apa pun.COMMIT: server mengosongkan antrean pemanggilan tertunda. Fungsicek_total_saldo()baru dijalankan sekarang, saat total sudah Rp4.500.000, sehingga transaksi dinyatakan sah.
Pola “melanggar dulu, kompensasi belakangan” ini adalah alasan utama fitur penundaan ada. Penyesuaian saldo antar-rekening, pemindahan baris antar-kelompok, dan proses impor bertahap sering membutuhkan jeda sebelum keadaan data menjadi sah.
Ketika COMMIT Itu Sendiri yang Gagal #
Apa yang terjadi bila transaksi berakhir tanpa memperbaiki pelanggarannya? Jawabannya dramatis: perintah COMMIT sendiri yang gagal. Pernyataan-pernyataan sebelumnya semuanya diterima, lalu seluruh transaksi dibatalkan pada detik terakhir:
BEGIN;
-- Total keluarga-santoso menjadi 5.400.000: pernyataan tetap diterima
INSERT INTO rekening (pemilik, kelompok, saldo)
VALUES ('Rina', 'keluarga-santoso', 900000);
COMMIT;Output yang Dihasilkan:
psql:komit-gagal.sql:5: ERROR: total saldo kelompok "keluarga-santoso" melebihi batas 5000000
CONTEXT: PL/pgSQL function cek_total_saldo() line 10 at RAISEPesan error muncul pada baris COMMIT, bukan pada baris INSERT. Karena pemeriksaan tertunda gagal, server membatalkan seluruh transaksi — baris Rina tidak pernah tersimpan, dan saldo anggota lain tidak tersentuh. File contoh berikutnya membuktikannya: SELECT pembuka di sana masih menampilkan empat baris yang sama, tanpa Rina.
SET CONSTRAINTS: Menunda Sesuai Permintaan #
Kadang trigger sebaiknya tegas secara bawaan, tetapi satu transaksi khusus perlu kelonggaran. Untuk itu ada perintah SET CONSTRAINTS, yang berlaku per transaksi dan memakai nama constraint trigger. Kita kembalikan trigger ke INITIALLY IMMEDIATE, lalu di dalam satu transaksi kita tunda pemeriksaannya secara eksplisit:
-- Baris Rina tidak pernah tersimpan: COMMIT yang gagal
-- membatalkan seluruh transaksi
SELECT id, pemilik, saldo FROM rekening ORDER BY id;
DROP TRIGGER cek_saldo_kelompok ON rekening;
-- Kembali ke INITIALLY IMMEDIATE, tetap DEFERRABLE
CREATE CONSTRAINT TRIGGER cek_saldo_kelompok
AFTER INSERT OR UPDATE ON rekening
DEFERRABLE INITIALLY IMMEDIATE
FOR EACH ROW EXECUTE FUNCTION cek_total_saldo();
BEGIN;
-- Tunda pemeriksaan hanya untuk transaksi ini
SET CONSTRAINTS cek_saldo_kelompok DEFERRED;
-- Total sementara 6.000.000: lolos karena pemeriksaan ditunda
UPDATE rekening SET saldo = 3000000 WHERE id = 2;
-- Kompensasi: total kembali menjadi 4.500.000
UPDATE rekening SET saldo = 900000 WHERE id = 1;
COMMIT;
SELECT id, pemilik, saldo FROM rekening ORDER BY id;Output yang Dihasilkan:
id | pemilik | saldo
----+---------+------------
1 | Budi | 2400000.00
2 | Siti | 1500000.00
3 | Dewi | 1000000.00
5 | Ani | 600000.00
(4 rows)
DROP TRIGGER
CREATE TRIGGER
BEGIN
SET CONSTRAINTS
UPDATE 1
UPDATE 1
COMMIT
id | pemilik | saldo
----+---------+------------
1 | Budi | 900000.00
2 | Siti | 3000000.00
3 | Dewi | 1000000.00
5 | Ani | 600000.00
(4 rows)Perintah SET CONSTRAINTS cek_saldo_kelompok DEFERRED hanya berlaku sampai transaksi berakhir — transaksi berikutnya kembali ke perilaku INITIALLY IMMEDIATE tanpa perlu mengatur ulang apa pun. Varian SET CONSTRAINTS ALL DEFERRED menunda seluruh constraint yang bisa ditunda sekaligus, dan lawannya SET CONSTRAINTS ALL IMMEDIATE memaksa pemeriksaan sisa berjalan di tengah transaksi — trik yang berguna untuk memindahkan error lebih dekat ke penyebabnya sebelum COMMIT.
Memilih Alat Validasi #
Constraint trigger melengkapi dua alat validasi yang sudah kita kenal di section Table Design. Pilihan alatnya mengikuti bentuk aturan dan momen pemeriksaan yang dibutuhkan:
| Alat | Bentuk aturan yang ditangani | Momen pemeriksaan | Bisa ditunda |
|---|---|---|---|
CHECK | syarat pada satu baris (kolomnya sendiri) | seketika, per baris | tidak |
EXCLUDE | relasi berpasangan antar baris lewat operator index | seketika, dibantu index | tidak |
| Constraint trigger | aturan bebas lewat query, termasuk agregat lintas baris | seketika atau saat COMMIT | ya |
Aturan “total saldo kelompok maksimal Rp5.000.000” jatuh pada baris ketiga karena aturan tersebut butuh SUM atas banyak baris — bukan perbandingan satu pasangan. Sebaliknya, aturan “shift dokter tidak boleh tumpang tindih” dari chapter Exclusion Constraints lebih baik tetap memakai EXCLUDE: pemeriksaannya ditopang index sehingga tetap cepat saat tabel membesar. Constraint trigger yang melakukan SUM penuh pada setiap tulis akan semakin mahal seiring bertambahnya anggota kelompok — pertimbangan biaya yang akan kita bahas tuntas di chapter Keterbatasan Trigger.
Dengan constraint trigger, kotak validasi Anda kini lengkap dari sisi data. Namun semua trigger yang kita bahas di section ini masih terikat pada satu tabel dan hanya melihat DML. Perintah CREATE TABLE, ALTER TABLE, dan DROP TABLE berjalan tanpa satu pun trigger bereaksi — kecuali kita naik satu level ke trigger yang mengawasi DDL itu sendiri, yang menjadi topik chapter Event Trigger berikutnya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
buat-tabel.sql
Unduh berkas-- Batas bisnis: total saldo satu kelompok maksimal Rp5.000.000
CREATE TABLE rekening (
id integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
pemilik text NOT NULL,
kelompok text NOT NULL,
saldo numeric(12,2) NOT NULL DEFAULT 0
);
CREATE FUNCTION cek_total_saldo() RETURNS trigger AS $$
DECLARE
total numeric;
BEGIN
SELECT sum(saldo) INTO total
FROM rekening
WHERE kelompok = NEW.kelompok;
IF total > 5000000 THEN
RAISE EXCEPTION 'total saldo kelompok "%" melebihi batas %',
NEW.kelompok, 5000000;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Constraint trigger: DEFERRABLE dan terdaftar dengan nama constraint
CREATE CONSTRAINT TRIGGER cek_saldo_kelompok
AFTER INSERT OR UPDATE ON rekening
DEFERRABLE INITIALLY IMMEDIATE
FOR EACH ROW EXECUTE FUNCTION cek_total_saldo();
INSERT INTO rekening (pemilik, kelompok, saldo) VALUES
('Budi', 'keluarga-santoso', 3000000),
('Siti', 'keluarga-santoso', 1500000),
('Dewi', 'keluarga-wijaya', 1000000);
SELECT id, pemilik, kelompok, saldo FROM rekening ORDER BY id;
\d rekening
ganti-deferred.sql
Unduh berkasDROP TRIGGER cek_saldo_kelompok ON rekening;
-- Versi tertunda: pemeriksaan digeser ke momen COMMIT
CREATE CONSTRAINT TRIGGER cek_saldo_kelompok
AFTER INSERT OR UPDATE ON rekening
DEFERRABLE INITIALLY DEFERRED
FOR EACH ROW EXECUTE FUNCTION cek_total_saldo();
BEGIN;
-- Total sementara 5.100.000 (melewati batas): pernyataan diterima, belum diperiksa
INSERT INTO rekening (pemilik, kelompok, saldo)
VALUES ('Ani', 'keluarga-santoso', 600000);
-- Kompensasi: turunkan saldo Budi sehingga total kembali 4.500.000
UPDATE rekening SET saldo = 2400000 WHERE id = 1;
-- Di dalam transaksi, keadaan yang "ilegal" itu benar-benar terlihat
SELECT id, pemilik, saldo FROM rekening ORDER BY id;
COMMIT;
-- Setelah COMMIT, pemeriksaan sudah dijalankan dan lolos
SELECT id, pemilik, saldo FROM rekening ORDER BY id;
komit-gagal.sql
Unduh berkasBEGIN;
-- Total keluarga-santoso menjadi 5.400.000: pernyataan tetap diterima
INSERT INTO rekening (pemilik, kelompok, saldo)
VALUES ('Rina', 'keluarga-santoso', 900000);
COMMIT;
pelanggaran-immediate.sql
Unduh berkas-- Total keluarga-santoso akan menjadi 5.500.000: langsung ditolak
INSERT INTO rekening (pemilik, kelompok, saldo)
VALUES ('Ani', 'keluarga-santoso', 1000000);
set-constraints.sql
Unduh berkas-- Baris Rina tidak pernah tersimpan: COMMIT yang gagal
-- membatalkan seluruh transaksi
SELECT id, pemilik, saldo FROM rekening ORDER BY id;
DROP TRIGGER cek_saldo_kelompok ON rekening;
-- Kembali ke INITIALLY IMMEDIATE, tetap DEFERRABLE
CREATE CONSTRAINT TRIGGER cek_saldo_kelompok
AFTER INSERT OR UPDATE ON rekening
DEFERRABLE INITIALLY IMMEDIATE
FOR EACH ROW EXECUTE FUNCTION cek_total_saldo();
BEGIN;
-- Tunda pemeriksaan hanya untuk transaksi ini
SET CONSTRAINTS cek_saldo_kelompok DEFERRED;
-- Total sementara 6.000.000: lolos karena pemeriksaan ditunda
UPDATE rekening SET saldo = 3000000 WHERE id = 2;
-- Kompensasi: total kembali menjadi 4.500.000
UPDATE rekening SET saldo = 900000 WHERE id = 1;
COMMIT;
SELECT id, pemilik, saldo FROM rekening ORDER BY id;