Trigger

Reaksi otomatis pada peristiwa INSERT, UPDATE, dan DELETE untuk pencatatan audit.

Daftar isi

Trigger adalah objek skema bernama yang diaktifkan server secara otomatis ketika peristiwa INSERT, UPDATE, atau DELETE terjadi pada tabel yang ditunjuknya. Topik ini membahas sintaks CREATE TRIGGER, perbedaan waktu BEFORE dan AFTER, akses kolom OLD dan NEW, serta penerapan trigger untuk pencatatan audit pada tabel terpisah. Contohnya memakai basis data perpustakaan yang dilengkapi tabel anggota_log untuk merekam setiap perubahan data anggota. Setelah mengikuti topik ini, perubahan data penting dapat tercatat otomatis tanpa mengubah satu pun pernyataan aplikasi.

Trigger pertama#

CREATE TRIGGER mendefinisikan trigger dengan urutan lima bagian, yaitu nama, waktu (BEFORE atau AFTER), peristiwa (INSERT, UPDATE, atau DELETE), tabel pemicunya, dan tubuh pernyataan yang dijalankan FOR EACH ROW. Sifat FOR EACH ROW menjadikan trigger berlevel baris, yaitu tubuhnya dijalankan sekali untuk setiap baris yang terdampak pernyataan pemicu. Satu UPDATE yang mengubah sepuluh baris menjalankan trigger yang sama sepuluh kali, sehingga volume operasi massal langsung memengaruhi beban trigger.

Jalankan persiapan berikut untuk membangun ulang basis data perpustakaan beserta tabel audit anggota_log.

sql persiapan-trigger.sql
-- tidak-dijalankan
-- Skrip persiapan topik trigger: basis data perpustakaan dengan tabel audit

DROP DATABASE IF EXISTS perpustakaan;
CREATE DATABASE perpustakaan;
USE perpustakaan;

CREATE TABLE anggota (
  id   INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(100) NOT NULL
);

CREATE TABLE buku (
  id      INT AUTO_INCREMENT PRIMARY KEY,
  judul   VARCHAR(200) NOT NULL,
  penulis VARCHAR(100) NOT NULL,
  stok    INT NOT NULL DEFAULT 0
);

CREATE TABLE peminjaman (
  id              INT AUTO_INCREMENT PRIMARY KEY,
  anggota_id      INT NOT NULL,
  buku_id         INT NOT NULL,
  tanggal_pinjam  DATE NOT NULL,
  jatuh_tempo     DATE NOT NULL,
  tanggal_kembali DATE NULL,
  FOREIGN KEY (anggota_id) REFERENCES anggota(id),
  FOREIGN KEY (buku_id) REFERENCES buku(id)
);

-- tabel audit untuk mencatat setiap perubahan data anggota
CREATE TABLE anggota_log (
  id         INT AUTO_INCREMENT PRIMARY KEY,
  anggota_id INT NOT NULL,
  aksi       VARCHAR(10) NOT NULL,
  nama       VARCHAR(100) NOT NULL,
  keterangan VARCHAR(200) NOT NULL
);

INSERT INTO anggota (nama) VALUES
  ('Ali'), ('Budi'), ('Citra'), ('Dewi'), ('Eka');

INSERT INTO buku (judul, penulis, stok) VALUES
  ('Dasar Astronomi untuk Pemula', 'Hartono', 3),
  ('Sejarah Maritim Nusantara', 'Wijaya', 2),
  ('Panduan Menanam Sayuran', 'Santoso', 4),
  ('Pengantar Fisika Klasik', 'Kusuma', 1),
  ('Cerita Rakyat Nusantara', 'Lestari', 3),
  ('Ilmu Gizi Sehari-hari', 'Pratiwi', 2);

INSERT INTO peminjaman (anggota_id, buku_id, tanggal_pinjam, jatuh_tempo, tanggal_kembali) VALUES
  (1, 1, '2026-05-02', '2026-05-16', '2026-05-15'),
  (2, 4, '2026-05-10', '2026-05-24', '2026-05-22'),
  (3, 2, '2026-06-05', '2026-06-19', NULL),
  (5, 5, '2026-06-14', '2026-06-28', NULL);
bash
# membuat ulang basis data perpustakaan dengan tabel audit anggota_log
mysql -u root < persiapan-trigger.sql

Trigger pertama mencatat setiap pendaftaran anggota baru ke tabel audit anggota_log. Tubuhnya berupa satu pernyataan INSERT, dan kolomnya mengambil nilai dari NEW, yaitu baris yang baru saja disimpan.

bash
# trigger audit AFTER INSERT pada tabel anggota
mysql -u root perpustakaan <<'SQL'
DROP TRIGGER IF EXISTS anggota_sisip;
CREATE TRIGGER anggota_sisip AFTER INSERT ON anggota
FOR EACH ROW
INSERT INTO anggota_log (anggota_id, aksi, nama, keterangan)
VALUES (NEW.id, 'INSERT', NEW.nama, 'pendaftaran anggota baru');
SQL
bash
# memverifikasi trigger dengan sisipan nyata
mysql -t -u root perpustakaan <<'SQL'
INSERT INTO anggota (nama) VALUES ('Fajar'), ('Gita');
SELECT id, anggota_id, aksi, nama, keterangan FROM anggota_log ORDER BY id;
SQL
text Keluaran
+----+------------+--------+-------+--------------------------+
| id | anggota_id | aksi   | nama  | keterangan               |
+----+------------+--------+-------+--------------------------+
|  1 |          6 | INSERT | Fajar | pendaftaran anggota baru |
|  2 |          7 | INSERT | Gita  | pendaftaran anggota baru |
+----+------------+--------+-------+--------------------------+

Setiap sisipan anggota memicu satu baris log, dan kolom anggota_id mengambil nilai NEW.id dari baris yang baru tersimpan. Lima anggota yang disisipkan oleh skrip persiapan tidak muncul di log karena trigger baru dibuat setelahnya. Trigger hanya bereaksi terhadap perubahan yang terjadi selama ia ada, sehingga urutan pembuatan trigger dan pengisian data menentukan cakupan audit.

Kolom OLD dan NEW#

Di dalam tubuh trigger, MySQL menyediakan dua baris semu. Kolom NEW memuat nilai yang akan disimpan pada peristiwa INSERT dan UPDATE, sedangkan kolom OLD memuat nilai sebelum perubahan pada peristiwa UPDATE dan DELETE. Ketersediaannya mengikuti peristiwa, sebagaimana tabel berikut.

PeristiwaOLDNEW
INSERTtidak adatersedia, dapat ditulis pada BEFORE
UPDATEtersediatersedia, dapat ditulis pada BEFORE
DELETEtersediatidak ada

NEW hanya dapat ditulis pada trigger BEFORE, sehingga perbaikan nilai dilakukan di sana sebelum baris tersimpan. Trigger berikut mengunci stok agar tidak pernah negatif, dengan mengganti nilai yang akan disimpan menjadi nol apabila sisipannya negatif.

bash
# trigger BEFORE INSERT memperbaiki nilai NEW sebelum baris tersimpan
mysql -u root perpustakaan <<'SQL'
DROP TRIGGER IF EXISTS buku_sebelum_sisip;
CREATE TRIGGER buku_sebelum_sisip BEFORE INSERT ON buku
FOR EACH ROW
SET NEW.stok = GREATEST(NEW.stok, 0);
SQL
bash
# stok negatif pada sisipan berubah menjadi nol
mysql -t -u root perpustakaan <<'SQL'
INSERT INTO buku (judul, penulis, stok) VALUES ('Atlas Hujan Tropis', 'Mahendra', -2);
SELECT id, judul, stok FROM buku WHERE id = 7;
SQL
text Keluaran
+----+--------------------+------+
| id | judul              | stok |
+----+--------------------+------+
|  7 | Atlas Hujan Tropis |    0 |
+----+--------------------+------+

Sisipan meminta stok negatif dua, tetapi baris yang tersimpan memuat nol karena GREATEST(NEW.stok, 0) menulis ulang kolom NEW sebelum penyimpanan. Kolom stok memang tidak memiliki constraint CHECK, sehingga trigger BEFORE menjadi lapisan penjaganya. Pada trigger AFTER, NEW hanya dapat dibaca karena barisnya sudah tersimpan, dan OLD selalu hanya dapat dibaca.

Untuk tabel dengan primary key yang tidak diubah oleh pembaruannya, OLD.id dan NEW.id bernilai sama pada peristiwa UPDATE, sehingga baris log dapat merujuk identitas yang tetap. Kolom-kolom ini juga hanya hidup di dalam tubuh trigger, dan nilai NEW yang terlihat pada trigger AFTER sudah final, yaitu nilai yang sungguh tersimpan termasuk nilai bawaan yang diisi server.

Audit tiga peristiwa#

Audit lengkap menuntut tiga trigger, satu untuk setiap peristiwa. Trigger pembaruan membandingkan OLD dan NEW pada kolom yang sama, sedangkan trigger penghapusan hanya memiliki OLD. Tubuh trigger boleh berupa satu pernyataan seperti pada contoh pertama, atau blok BEGIN...END berisi banyak pernyataan, dan begitu tubuhnya memuat ;, DELIMITER kembali diperlukan dengan cara yang sama seperti saat membuat prosedur.

Dua trigger berikut melengkapi audit tabel anggota, keduanya dengan AFTER dan tubuh blok BEGIN...END.

bash
# trigger audit AFTER UPDATE dan AFTER DELETE pada tabel anggota
mysql -u root perpustakaan <<'SQL'
DELIMITER //

DROP TRIGGER IF EXISTS anggota_ubah //

CREATE TRIGGER anggota_ubah AFTER UPDATE ON anggota
FOR EACH ROW
BEGIN
  INSERT INTO anggota_log (anggota_id, aksi, nama, keterangan)
  VALUES (OLD.id, 'UPDATE', NEW.nama,
          CONCAT('nama berubah dari "', OLD.nama, '" menjadi "', NEW.nama, '"'));
END //

DROP TRIGGER IF EXISTS anggota_hapus //

CREATE TRIGGER anggota_hapus AFTER DELETE ON anggota
FOR EACH ROW
BEGIN
  INSERT INTO anggota_log (anggota_id, aksi, nama, keterangan)
  VALUES (OLD.id, 'DELETE', OLD.nama, CONCAT('anggota ', OLD.nama, ' diarsipkan'));
END //

DELIMITER ;
SQL

Verifikasi dilakukan dengan perubahan nyata, yaitu satu pembaruan nama dan satu pengarsipan anggota. Sebelum anggota dihapus, baris peminjamannya dihapus terlebih dahulu, karena foreign key anggota_id tidak mengizinkan penghapusan anggota yang masih memilikinya.

bash
# pembaruan dan penghapusan nyata untuk mengisi log audit
mysql -t -u root perpustakaan <<'SQL'
UPDATE anggota SET nama = 'Ali Yudhistira' WHERE id = 1;
DELETE FROM peminjaman WHERE anggota_id = 5;
DELETE FROM anggota WHERE id = 5;
SELECT id, anggota_id, aksi, nama, keterangan FROM anggota_log ORDER BY id;
SQL
text Keluaran
+----+------------+--------+----------------+--------------------------------------------------+
| id | anggota_id | aksi   | nama           | keterangan                                       |
+----+------------+--------+----------------+--------------------------------------------------+
|  1 |          6 | INSERT | Fajar          | pendaftaran anggota baru                         |
|  2 |          7 | INSERT | Gita           | pendaftaran anggota baru                         |
|  3 |          1 | UPDATE | Ali Yudhistira | nama berubah dari "Ali" menjadi "Ali Yudhistira" |
|  4 |          5 | DELETE | Eka            | anggota Eka diarsipkan                           |
+----+------------+--------+----------------+--------------------------------------------------+

Empat baris log kini mencatat tiga jenis peristiwa. Baris UPDATE memuat nama baru pada kolom nama dan menyebut nama lama pada keterangannya, karena OLD dan NEW sama-sama tersedia pada trigger pembaruan. Baris DELETE hanya dapat merujuk OLD, sehingga identitas terakhir anggota Eka terekam sebelum hilang.

AFTER dipilih untuk audit karena baris log hanya ditulis setelah perubahan benar-benar tersimpan. Pekerjaan trigger dan pernyataan pemicunya berada dalam satu transaksi, sehingga apabila pernyataan pemicunya gagal dan di-ROLLBACK, baris lognya ikut dibatalkan. Dengan demikian log tidak pernah mencatat perubahan yang tidak pernah terjadi.

Mengelola trigger#

Daftar trigger pada sebuah skema dapat dibaca dari kamus data information_schema.triggers, dengan memilih kolom yang menunjukkan peristiwa, tabel, dan waktunya. Klien mysql juga menyediakan SHOW TRIGGERS, tetapi keluarannya memuat kolom waktu pembuatan sehingga tidak cocok untuk perbandingan yang menuntut keluaran tetap. Nama trigger bersifat unik di tingkat skema, bukan di tingkat tabel, dan penghapusannya memakai DROP TRIGGER.

bash
# daftar trigger pada skema perpustakaan dari kamus data
mysql -t -u root perpustakaan -e "SELECT trigger_name, event_manipulation, event_object_table, action_timing FROM information_schema.triggers WHERE trigger_schema = 'perpustakaan' ORDER BY trigger_name;"
text Keluaran
+--------------------+--------------------+--------------------+---------------+
| TRIGGER_NAME       | EVENT_MANIPULATION | EVENT_OBJECT_TABLE | ACTION_TIMING |
+--------------------+--------------------+--------------------+---------------+
| anggota_hapus      | DELETE             | anggota            | AFTER         |
| anggota_sisip      | INSERT             | anggota            | AFTER         |
| anggota_ubah       | UPDATE             | anggota            | AFTER         |
| buku_sebelum_sisip | INSERT             | buku               | BEFORE        |
+--------------------+--------------------+--------------------+---------------+

Empat trigger terdaftar. Tiga di antaranya terpasang pada tabel anggota untuk setiap peristiwa, sedangkan satu lagi terpasang pada tabel buku. Kolom EVENT_MANIPULATION dan ACTION_TIMING bersama-sama menyatakan posisi trigger dalam pola waktu peristiwa, misalnya AFTER untuk INSERT. Pola DROP TRIGGER IF EXISTS sebelum CREATE TRIGGER membuat skrip penyiapan tetap dapat dijalankan ulang.

Kamus data yang sama juga memuat kolom ACTION_STATEMENT berisi tubuh lengkap setiap trigger, sehingga definisi yang ditulis lama tetap dapat ditinjau tanpa membuka skrip aslinya. Menghapus trigger tidak menyentuh satu baris data pun pada tabel pemicunya. Yang hilang hanyalah reaksi otomatisnya. Tabel audit yang sudah terisi tetap utuh setelah trigger dihapus, dan baris log lama tetap terbaca seperti biasa.

Kesalahan umum#

Kesalahan pertama adalah membuat trigger dengan nama yang sudah terpakai di skema yang sama. Karena namespace-nya setingkat skema, nama yang sama tetap ditolak meskipun tabel pemicunya berbeda.

bash
# nama trigger yang sama dalam satu skema ditolak server
mysql -u root perpustakaan -e "CREATE TRIGGER anggota_sisip AFTER INSERT ON anggota FOR EACH ROW INSERT INTO anggota_log (anggota_id, aksi, nama, keterangan) VALUES (NEW.id, 'INSERT', NEW.nama, 'duplikat');"
text Keluaran
ERROR 1359 (HY000) at line 1: Trigger already exists

Kesalahan kedua adalah trigger yang mengubah tabel pemicunya sendiri, misalnya mencoba memperbarui peminjaman dari dalam trigger AFTER INSERT ON peminjaman. Pernyataan pemicu masih memakai tabel tersebut, sehingga perubahan di tengah pernyataan yang sama tidak mungkin dilakukan dan server menolaknya saat sisipan berjalan.

bash
# trigger yang mengubah tabel pemicunya sendiri
mysql -u root perpustakaan <<'SQL'
DELIMITER //

DROP TRIGGER IF EXISTS peminjaman_perbarui_sendiri //

CREATE TRIGGER peminjaman_perbarui_sendiri AFTER INSERT ON peminjaman
FOR EACH ROW
BEGIN
  UPDATE peminjaman SET jatuh_tempo = jatuh_tempo WHERE id = NEW.id;
END //

DELIMITER ;
SQL

mysql -u root perpustakaan -e "INSERT INTO peminjaman (anggota_id, buku_id, tanggal_pinjam, jatuh_tempo) VALUES (1, 2, '2026-07-02', '2026-07-16');"
text Keluaran
ERROR 1442 (HY000) at line 1: Can't update table 'peminjaman' in stored function/trigger because it is already used by statement which invoked this stored function/trigger.

Perbaikannya bukan memindahkan perubahan ke trigger lain, melainkan menulisnya langsung di pernyataan aplikasi atau menyimpan hasilnya ke tabel yang berbeda. Trigger peraga tersebut dihapus agar tidak mengganggu sisipan berikutnya pada basis data latihan.

bash
# menghapus trigger peraga
mysql -u root perpustakaan -e "DROP TRIGGER IF EXISTS peminjaman_perbarui_sendiri;"

Kesalahan ketiga adalah menulis tubuh trigger tanpa FOR EACH ROW, yang berujung error 1064 karena klausanya wajib ada. Sifat per barisnya juga perlu diperhitungkan, karena pernyataan yang menyentuh ribuan baris menjalankan tubuh trigger sebanyak itu pula, sehingga logika yang berat di dalamnya memperlambat seluruh pernyataan pemicunya. Kesalahan keempat adalah membiarkan skrip tanpa DROP TRIGGER IF EXISTS, sehingga skrip hanya berjalan sekali dan gagal dengan error 1359 pada eksekusi berikutnya.

Ringkasan#

  • Trigger adalah objek bernama yang dijalankan otomatis untuk setiap baris terdampak ketika peristiwa INSERT, UPDATE, atau DELETE terjadi pada tabel pemicunya.
  • Kolom NEW memuat nilai yang akan disimpan dan hanya dapat ditulis pada trigger BEFORE, sedangkan OLD memuat nilai sebelum perubahan dan hanya ada pada UPDATE serta DELETE.
  • Pencatatan audit lazim memakai tiga trigger AFTER yang menulis ke tabel log terpisah, dan baris lognya ikut ter-ROLLBACK apabila pernyataan pemicunya gagal.
  • Daftar trigger dapat dibaca dari information_schema.triggers, dan pola DROP TRIGGER IF EXISTS menjaga skrip tetap dapat dijalankan ulang.

Referensi#