Chapter sebelumnya memperkenalkan trigger sebagai reaksi otomatis dengan satu pilihan waktu: AFTER. Padahal klausa AFTER INSERT ON produk sebenarnya hanya setengah dari kisah waktu — MySQL juga menyediakan BEFORE, saat baris baru masih berupa konsep yang bisa kita ubah sebelum benar-benar tersimpan. Dua waktu ini membuka dua peran berbeda: BEFORE cocok untuk memvalidasi dan merapikan data yang hendak masuk, sedangkan AFTER cocok untuk mereaksi data yang sudah final. Pada chapter ini kita membuat kedua jenis trigger pada tabel yang sama, membuktikan nyata bahwa BEFORE sanggup mengubah nilai yang tersimpan, lalu mengamati urutan eksekusinya.

Menyiapkan Tabel Latihan #

Kita pakai kembali database belajar_trig dengan tabel produk dan audit_log seperti chapter sebelumnya. Setiap chapter di section ini berdiri mandiri, jadi struktur lengkapnya kita bangun dari nol:

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)

BEFORE INSERT: Mengubah Data Sebelum Disimpan #

Trigger BEFORE INSERT menyala sebelum baris baru dituliskan ke tabel. Pada momen itu, baris calon sudah terbentuk sebagai alias NEW, dan nilai-nilainya masih boleh kita timpa. Kita manfaatkan momen tersebut untuk merapikan dua masalah input yang klasik: spasi berlebih di awal dan akhir nama, serta harga pecahan yang tidak baku:

sql
DELIMITER ;;

CREATE TRIGGER rapikan_produk
BEFORE INSERT ON produk
FOR EACH ROW
BEGIN
  SET NEW.nama = TRIM(NEW.nama);
  SET NEW.harga = ROUND(NEW.harga);
END;;

DELIMITER ;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Baris SET NEW.nama = TRIM(NEW.nama) membaca nilai nama yang dikirim aplikasi, membuang spasi di kedua ujungnya lewat fungsi TRIM(), lalu menulis hasilnya kembali ke alias NEW. Baris SET NEW.harga = ROUND(NEW.harga) melakukan hal serupa pada harga: nilai pecahan dibulatkan ke bilangan bulat rupiah terdekat. Perubahan ini terjadi sebelum MySQL menulis baris, jadi tabel hanya akan pernah melihat versi yang sudah rapi.

Bukti terbaiknya adalah mengirim data yang sengaja kotor, lalu membaca isi tabel:

sql
INSERT INTO produk (nama, harga, stok) VALUES ('  Kopi Tubruk  ', 54999.99, 7);

SELECT id, nama, harga, stok FROM produk ORDER BY id;

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

+----+-------------+----------+------+
| id | nama        | harga    | stok |
+----+-------------+----------+------+
|  1 | Kopi Tubruk | 55000.00 |    7 |
+----+-------------+----------+------+
1 row in set (0.00 sec)

Aplikasi mengirim nama ' Kopi Tubruk ' dengan spasi ganda di kedua ujung dan harga 54999.99, namun tabel menyimpan 'Kopi Tubruk' dan 55000.00. Nilai yang berbeda dari input tersebut hanya bisa dijelaskan oleh satu aktor: trigger BEFORE INSERT yang menyala di tengah jalan. Normalisasi seperti ini sangat berguna ketika data masuk dari banyak aplikasi berbeda — aturan rapikannya cukup ditulis satu kali di database, bukan diulang di setiap aplikasi.

AFTER INSERT: Bereaksi pada Data yang Sudah Final #

Sekarang kita pasang trigger kedua pada kejadian yang sama — INSERT pada produk — tetapi dengan waktu AFTER. Isinya pencatatan audit seperti chapter sebelumnya:

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)

Dua trigger kini hidup berdampingan pada kejadian INSERT. MySQL selalu menjalankan seluruh trigger BEFORE lebih dulu, menuliskan baris ke tabel, lalu menjalankan seluruh trigger AFTER. Urutan tersebut membuat trigger AFTER selalu membaca nilai final alias NEW — termasuk hasil perubahan yang dilakukan trigger BEFORE. Kita buktikan dengan mengirim data kotor sekali lagi:

sql
INSERT INTO produk (nama, harga, stok) VALUES ('  Gula Aren  ', 15499.99, 20);

SELECT id, nama, harga, stok FROM produk ORDER BY id;

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

Output yang Dihasilkan:

text
Query OK, 1 row affected (0.00 sec)

+----+-------------+----------+------+
| id | nama        | harga    | stok |
+----+-------------+----------+------+
|  1 | Kopi Tubruk | 55000.00 |    7 |
|  2 | Gula Aren   | 15500.00 |   20 |
+----+-------------+----------+------+
2 rows in set (0.00 sec)

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

Tabel produk menerima 'Gula Aren' dan 15500.00 — rapi, bukti trigger rapikan_produk bekerja. Baris audit juga berbunyi produk baru: Gula Aren tanpa spasi ganda, bukti trigger audit_produk_insert berjalan setelah perapian selesai. Kalau urutannya terbalik (audit menyala sebelum rapikan), baris audit akan menyimpan nama mentah dengan spasi — output kita menunjukkan kebalikannya, sehingga urutan BEFORE → simpan → AFTER terbukti dari data.

Mengubah NEW di AFTER Ditolak Server #

Karena NEW terbaca di kedua waktu, kita mungkin mengira NEW juga bisa diubah kapan saja. MySQL menolak tegas penulisan NEW pada trigger AFTER:

sql
-- @error
CREATE TRIGGER ubah_di_after AFTER INSERT ON produk FOR EACH ROW SET NEW.harga = 1.00;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1362 (HY000): Updating of NEW row is not allowed in after trigger
Bye

ERROR 1362 menjelaskan alasannya: baris sudah tersimpan saat trigger AFTER berjalan, sehingga tidak ada lagi “baris baru” yang bisa diubah lewat NEW. Satu-satunya jendela untuk menulis ke NEW adalah trigger BEFORE, sebelum baris menempel di tabel. Alias OLD bahkan lebih ketat lagi: alias tersebut hanya bisa dibaca, tidak pernah bisa ditulis, karena baris lama hanyalah salinan sejarah. Kalau data harus diubah, jalannya selalu BEFORE dan targetnya selalu NEW.

Kapan Memilih BEFORE atau AFTER #

Pilihan waktu menentukan peran trigger secara keseluruhan. Tabel berikut merangkum pertimbangan praktisnya:

AspekBEFOREAFTER
Kondisi baris di tabelbelum tersimpansudah tersimpan
Membaca NEW / OLDbisabisa
Menulis ke NEWbisaditolak (ERROR 1362)
Cocok untukvalidasi, normalisasi, pemblokiran dataaudit, pencatatan, konsistensi turunan
Jika isi trigger errorbaris batal masukstatement dibatalkan, baris ikut ter-rollback

Aturan praktisnya sederhana. Gunakan BEFORE setiap kali keputusan harus memengaruhi baris itu sendiri: menolak harga aneh, membulatkan nilai, atau mengisi kolom turunan sebelum disimpan. Gunakan AFTER setiap kali reaksi tidak perlu mengubah baris yang bersangkutan: menulis log, memperbarui counter di tabel lain, atau menjaga data turunan tetap segar. Memanggil SIGNAL di BEFORE membuat baris tidak pernah tersimpan; SIGNAL di AFTER membatalkan seluruh statement — perbedaan halik yang kita uji di chapter integritas data.

NEW dan OLD sebagai Alias Baris #

Sepanjang chapter ini kita memakai NEW tanpa mendefinisikannya di mana pun — memang tidak perlu. MySQL otomatis menyediakan dua alias untuk setiap baris yang memicu trigger: NEW menampung nilai baru (calon baris pada INSERT, hasil akhir pada UPDATE), sedangkan OLD menampung nilai lama (baris sebelum diubah pada UPDATE, baris yang terhapus pada DELETE). Ketersediaannya mengikuti kejadian:

KejadianNEWOLD
INSERTtersediatidak tersedia
UPDATEtersediatersedia
DELETEtidak tersediatersedia

Logikanya konsisten: INSERT tidak punya masa lalu sehingga OLD tidak bermakna, dan DELETE tidak punya masa depan di tabel sehingga NEW tidak bermakna. MySQL memeriksa pelanggaran aturan ini saat pembuatan trigger — mencoba membaca OLD pada trigger INSERT langsung ditolak dengan ERROR 1363. Perbandingan OLD lawan NEW justru menjadi bahan paling berharga untuk audit perubahan, dan itulah fokus chapter berikutnya.

Membersihkan Database Latihan #

Kedua trigger beserta tabel latihannya kita jatuhkan bersama:

sql
DROP DATABASE belajar_trig;

Output yang Dihasilkan:

text
Query OK, 2 rows affected (0.00 sec)

Referensi #

BEFORE dan AFTER kini punya wajah yang jelas: yang pertama mengubah NEW sebelum baris menempel di tabel, yang kedua membaca data final dan bereaksi. Namun contoh kita masih bermain di kejadian INSERT saja. Tiga kejadian lainnya — UPDATE dengan duel OLD lawan NEW, serta DELETE yang menyimpan baris terakhir dalam OLD — menyimpan pola audit yang jauh lebih kaya. Chapter berikutnya mengurai ketiganya satu per satu.