Sejauh ini setiap perubahan data yang kita lakukan selalu disertai langkah manual: kalau ingin mencatat kejadian ke tabel log, kita harus mengingat diri untuk menulis INSERT kedua setelah INSERT pertama. Begitu satu saja programmer aplikasi lupa, celah audit langsung terbentuk dan tidak ada yang tahu baris mana yang terlewat. MySQL menyediakan mekanisme bernama trigger untuk menutup celah semacam itu: sebuah blok kode yang tersimpan di dalam database dan menyala sendiri setiap kali sebuah kejadian tertentu menimpa tabel. Pada chapter ini kita membangun trigger pertama, membuktikan bahwa audit_log terisi tanpa perintah tambahan apa pun, lalu mempelajari cara memeriksa dan menghapus trigger tersebut.

Trigger Adalah Kode yang Menyala Otomatis #

Trigger adalah objek database yang melekat pada satu tabel dan terikat pada satu kejadian. Kejadian yang dipilihkan hanya tiga macam: INSERT, UPDATE, atau DELETE. Selain kejadian, trigger juga memilih waktu menyala: BEFORE artinya kode berjalan sebelum baris benar-benar diubah di tabel, sedangkan AFTER artinya kode berjalan setelah baris selesai diubah. Kombinasi keduanya menghasilkan enam variasi trigger untuk setiap tabel, misalnya BEFORE UPDATE atau AFTER DELETE.

Tiga sifat trigger membedakan trigger dari stored procedure yang pernah kita bahas di section sebelumnya:

  • Otomatis. Trigger menyala karena adanya statement INSERT, UPDATE, atau DELETE — tidak ada perintah khusus untuk menjalankannya.
  • Per baris. Sebuah INSERT yang menambah lima baris akan menyalakan trigger lima kali, sekali untuk setiap baris.
  • Melekat pada tabel. Trigger tidak bisa dibuat pada view dan tidak bisa dipindahkan; trigger ikut terhapus ketika tabel atau database tempatnya tinggal dihapus.

Alur kerjanya dapat kita petakan seperti diagram berikut:

Ketika sebuah statement DML tiba di server, MySQL memeriksa daftar trigger milik tabel tersebut lebih dulu. Jika ada yang cocok dengan kejadian dan waktunya, isi trigger dijalankan sebagai bagian dari statement yang sama — keberhasilan atau kegagalan trigger ikut menentukan nasib statement, tema yang kita uji nyata di chapter debugging.

Menyiapkan Tabel Latihan #

Kita pakai database belajar_trig dengan dua tabel: produk sebagai tabel utama dan audit_log sebagai tabel pencatat kejadian. Kolom dibuat pada audit_log bertipe TIMESTAMP dengan bawaan CURRENT_TIMESTAMP supaya setiap baris audit otomatis tercatat waktunya:

sql
DROP DATABASE IF EXISTS belajar_trig;
CREATE DATABASE belajar_trig;
USE belajar_trig;

Output yang Dihasilkan:

text
Query OK, 0 rows affected, 1 warning (0.00 sec)

Query OK, 1 row affected (0.00 sec)

Database changed
sql
CREATE TABLE produk (
  id INT AUTO_INCREMENT PRIMARY KEY,
  nama VARCHAR(100) NOT NULL,
  harga DECIMAL(10,2) NOT NULL,
  stok INT NOT NULL DEFAULT 0
);

CREATE TABLE audit_log (
  id INT AUTO_INCREMENT PRIMARY KEY,
  aksi VARCHAR(20) NOT NULL,
  detail VARCHAR(255) NOT NULL,
  dibuat TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Trigger Pertama: Audit Otomatis setelah INSERT #

Trigger pertama kita menempel di tabel produk dan menyala setelah setiap INSERT baru. Isinya satu statement: mencatat nama produk ke audit_log. Karena isi trigger memuat tanda titik koma, seluruh definisi harus dibungkus perintah DELIMITER dalam satu blok:

sql
DELIMITER ;;

CREATE TRIGGER audit_produk_insert
AFTER INSERT ON produk
FOR EACH ROW
BEGIN
  INSERT INTO audit_log (aksi, detail)
  VALUES ('INSERT', CONCAT('produk baru: ', NEW.nama));
END;;

DELIMITER ;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Mari kita bedah definisi di atas baris per baris:

  1. DELIMITER ;; mengubah penanda akhir statement milik client mysql dari ; menjadi ;;. Tanpa perubahan ini, client akan menganggap statement CREATE TRIGGER selesai di titik koma pertama di dalam BEGIN ... END, dan definisi trigger yang terpotong akan ditolak sebagai sintaks salah.
  2. CREATE TRIGGER audit_produk_insert memberi nama trigger. Nama inilah yang dipakai saat kita ingin memeriksa atau menghapus trigger nanti.
  3. AFTER INSERT ON produk menentukan dua hal sekaligus: kejadian yang menyalakan trigger (INSERT) dan tabel tempat trigger melekat (produk), dengan waktu AFTER karena kita ingin mencatat kejadian yang sudah benar-benar terjadi.
  4. FOR EACH ROW menyatakan trigger berjalan satu kali untuk setiap baris yang ditambahkan. Statement INSERT multi-baris menyalakan trigger sejumlah barisnya.
  5. BEGIN ... END membungkus isi trigger. Blok ini wajib begitu isi trigger berisi lebih dari satu statement, dan kebiasaan memakainya sejak awal membuat definisi mudah ditambah kemudian.
  6. INSERT INTO audit_log ... adalah isi trigger: mencatat string INSERT beserta nama produk. NEW.nama merujuk kolom nama pada baris yang baru saja masuk — setiap baris yang memicu trigger dapat kita baca lewat alias NEW.
  7. DELIMITER ; mengembalikan penanda akhir statement ke ; setelah definisi trigger selesai dengan END;;.

Membuktikan Trigger Menyala Sendiri #

Bukti terbaik adalah mengambil data tanpa menyentuh audit_log secara manual. Kita cukup menambah satu produk seperti biasa, lalu membaca isi audit_log:

sql
INSERT INTO produk (nama, harga, stok) VALUES ('Kopi Gayo', 55000.00, 10);

SELECT id, aksi, detail FROM audit_log ORDER BY id;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

+----+--------+------------------------+
| id | aksi   | detail                 |
+----+--------+------------------------+
|  1 | INSERT | produk baru: Kopi Gayo |
+----+--------+------------------------+
1 row in set (0.00 sec)

Tabel audit_log berisi satu baris produk baru: Kopi Gayo padahal kita tidak pernah menulis INSERT INTO audit_log secara langsung. Kolom dibuat juga sudah terisi CURRENT_TIMESTAMP oleh bawaan kolom, jadi pencatatan waktu tidak bergantung pada aplikasi. Inilah jaminan yang dicari: selama penambahan produk lewat INSERT, catatan audit pasti ikut terbentuk — tidak ada lagi langkah manual yang bisa terlewat.

Memeriksa Trigger dengan SHOW TRIGGERS #

Daftar trigger yang tinggal di database bisa kita lihat dengan SHOW TRIGGERS:

sql
-- @check
SHOW TRIGGERS;

Output yang Dihasilkan:

text
+---------------------+--------+--------+----------------------------------------------------------------------------------------------------------+--------+------------------------+-----------------------------------------------------------------------------------------------------------------------+----------------+----------------------+----------------------+--------------------+
| Trigger             | Event  | Table  | Statement                                                                                                | Timing | Created                | sql_mode                                                                                                              | Definer        | character_set_client | collation_connection | Database Collation |
+---------------------+--------+--------+----------------------------------------------------------------------------------------------------------+--------+------------------------+-----------------------------------------------------------------------------------------------------------------------+----------------+----------------------+----------------------+--------------------+
| audit_produk_insert | INSERT | produk | BEGIN
  INSERT INTO audit_log (aksi, detail)
  VALUES ('INSERT', CONCAT('produk baru: ', NEW.nama));
END | AFTER  | 2026-10-14 12:50:17.39 | ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION | root@localhost | utf8mb4              | utf8mb4_0900_ai_ci   | utf8mb4_0900_ai_ci |
+---------------------+--------+--------+----------------------------------------------------------------------------------------------------------+--------+------------------------+-----------------------------------------------------------------------------------------------------------------------+----------------+----------------------+----------------------+--------------------+
1 row in set (0.00 sec)

Baris tersebut merangkum seluruh identitas trigger: nama, kejadian INSERT, tabel produk, isi statement, waktu AFTER, hingga definer yang membuatnya. Kolom Created berisi waktu pembuatan trigger sehingga nilainya pasti berbeda pada server Anda — itulah sebabnya blok ini ditandai @check dan outputnya tidak dibandingkan verbatim. Kolom sql_mode ikut terekam karena trigger menyimpan mode SQL saat pembuatannya, perilaku yang kita bahas lebih dalam di chapter debugging.

Menghapus Trigger dengan DROP TRIGGER #

Trigger dihapus memakai namanya, bukan nama tabelnya. Pola idempoten IF EXISTS mencegah error saat trigger memang sudah tidak ada:

sql
DROP TRIGGER IF EXISTS audit_produk_insert;

INSERT INTO produk (nama, harga, stok) VALUES ('Teh Melati', 25000.00, 5);

SELECT COUNT(*) AS baris_audit FROM audit_log;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 1 row affected (0.00 sec)

+-------------+
| baris_audit |
+-------------+
|           1 |
+-------------+
1 row in set (0.00 sec)

Setelah trigger dijatuhkan, penambahan Teh Melati berjalan tanpa error namun jumlah baris audit_log tetap satu. Perhatikan urutan pembuktian ini: kita tidak sekadar memercayai DROP TRIGGER berhasil, melainkan membuktikan efeknya lewat SELECT. Ketika trigger mengganggu dan ingin dites ulang, melepasnya dengan DROP TRIGGER adalah langkah pertama isolasi yang sah sebelum versi trigger yang diperbaiki dibuat ulang.

Trigger Tidak Bisa Dipanggil Manual #

Karena trigger punya nama, kita mungkin tergoda memperlakukannya seperti stored procedure. Percobaan memanggilnya lewat CALL langsung ditolak:

sql
-- @error
CALL audit_produk_insert;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1305 (42000): PROCEDURE belajar_trig.audit_produk_insert does not exist
Bye

MySQL menjawab bahwa procedure bernama audit_produk_insert tidak ada — perintah CALL memang hanya mencari stored procedure, dan trigger tidak terdaftar di katalog procedure sama sekali. Trigger hanya punya satu pintu masuk: statement INSERT, UPDATE, atau DELETE pada tabel yang sesuai. Tidak ada mekanisme menjadwalkan trigger untuk besok pagi, tidak ada parameter yang bisa dikirim, dan tidak ada cara menyalakannya untuk satu baris tertentu saja. Sifat satu-pintu inilah yang membuat trigger mudah dianalisis: semua jalur yang bisa menyalakannya terlihat dari definisi tabelnya.

Membersihkan Database Latihan #

Latihan selesai dan kita jatuhkan database latihan beserta seluruh objeknya:

sql
DROP DATABASE belajar_trig;

Output yang Dihasilkan:

text
Query OK, 2 rows affected (0.00 sec)

Referensi #

Trigger terbukti menjadi penjaga yang patuh: sekali terpasang, setiap INSERT baru otomatis meninggalkan jejak di audit_log tanpa perintah tambahan. Namun contoh kita baru memakai satu waktu menyala, AFTER. Pertanyaan berikutnya adalah apa yang berubah bila trigger menyala BEFORE baris masuk — khususnya, apakah trigger boleh mengubah data yang sedang ditambahkan sebelum data tersebut tersimpan. Chapter berikutnya mengadu kedua waktu tersebut secara langsung.