Trigger dan event membuat database terasa hidup — data tervalidasi, tercatat, dan terrawat tanpa campur tangan aplikasi. Namun MySQL memasang pagar-pagar keras di sekeliling kekuatan itu: ada statement yang tidak boleh dijalankan trigger, ada objek yang tidak boleh memuat trigger, dan ada kondisi server yang membuat event mati suri. Memahami batasan ini sama pentingnya dengan memahami fiturnya, karena desain yang melanggar pagar-pagar tersebut akan gagal di tengah jalan — bukan saat desain disusun, melainkan saat statement pertama dieksekusi. Chapter penutup ini menguji setiap larangan secara nyata, merangkumnya dalam satu tabel, lalu menutup section dengan panduan kapan trigger dan event sebaiknya tidak dipakai.

Menyiapkan Tabel Latihan #

Batasan paling mudah dipahami lewat percobaan langsung. Kita siapkan tabel produk, tabel catatan sebagai target tulisan, sebuah stored procedure, dan sebuah view:

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

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 catatan (
  id INT AUTO_INCREMENT PRIMARY KEY,
  pesan VARCHAR(100) NOT NULL
);

CREATE VIEW v_produk AS SELECT id, nama FROM produk;

Output yang Dihasilkan:

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

Query OK, 1 row affected (0.00 sec)

Database changed
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Trigger Tidak Boleh Mengembalikan Result Set #

Trigger menyala di dalam mesin server, jauh dari layar siapa pun — karena itu MySQL melarang trigger mengembalikan baris hasil. Mencoba menaruh SELECT telanjang di isi trigger ditolak sejak saat pembuatan:

sql
-- @error
CREATE TRIGGER trig_tampil AFTER INSERT ON produk FOR EACH ROW SELECT COUNT(*) FROM produk;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1415 (0A000): Not allowed to return a result set from a trigger
Bye

ERROR 1415 tegas: result set tidak boleh keluar dari trigger. Larangan ini juga menutup jalan memutar lewat stored procedure. Procedure boleh mengembalikan result set, tetapi procedure yang dipanggil trigger tetap berada di dalam mesin — hasilnya tetap terjebak. Kita buktikan dengan procedure penghitung. Karena procedure memuat titik koma di dalam BEGIN ... END, keduanya kita tulis dalam satu blok DELIMITER — perhatikan bahwa isi trigger CALL hitung_produk() juga ditutup ;; mengikuti delimiter yang sedang aktif:

sql
DELIMITER ;;

CREATE PROCEDURE hitung_produk()
BEGIN
  SELECT COUNT(*) AS jumlah FROM produk;
END;;

CREATE TRIGGER trig_panggil_proc
AFTER INSERT ON produk
FOR EACH ROW
CALL hitung_produk();;

DELIMITER ;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

Query OK, 0 rows affected (0.00 sec)

Pembuatan procedure dan trigger keduanya diterima — pagar baru terasa saat trigger menyala:

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

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1415 (0A000): Not allowed to return a result set from a trigger
Bye

Error yang sama, ERROR 1415, kini muncul dari statement pemanggil. Trigger yang memanggil procedure masih sah selama procedure tidak mengembalikan result set — procedure penghitung yang menulis hasilnya ke tabel catatan akan berjalan mulus. Perbedaan keduanya penting: trigger dan procedure boleh bekerja sama menghasilkan efek, tetapi tidak boleh bekerja sama menghasilkan tampilan.

Trigger Hanya Menempel pada Tabel Dasar #

Klausa ON pada CREATE TRIGGER hanya menerima tabel dasar. View — yang kita kenal sebagai jendela baca terhadap tabel — tidak bisa memiliki trigger:

sql
-- @error
CREATE TRIGGER trig_view AFTER INSERT ON v_produk FOR EACH ROW INSERT INTO catatan (pesan) VALUES ('dari view');

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1347 (HY000): 'belajar_trig.v_produk' is not BASE TABLE
Bye

ERROR 1347 menolak v_produk karena objek tersebut bukan base table. Alasannya struktural: trigger menyala pada perubahan baris fisik di tabel, sedangkan view tidak menyimpan baris — setiap INSERT lewat view yang bisa-diperbarui diteruskan ke tabel dasar, dan trigger tabel dasar itulah yang menyala. Membuat trigger pada tabel dasar dengan demikian sudah otomatis mengawasi seluruh jalur perubahan, termasuk jalur yang melewati view.

Transaksi Eksplisit Dilarang dalam Trigger #

Trigger selalu berjalan di dalam statement pemanggilnya, dan statement tersebut bisa menjadi bagian transaksi yang lebih besar. Karena itu trigger tidak boleh mengatur titik commit sendiri:

sql
-- @error
CREATE TRIGGER trig_commit AFTER INSERT ON produk FOR EACH ROW COMMIT;

Output yang Dihasilkan:

text
Query OK, 0 rows affected (0.00 sec)

ERROR 1422 (HY000): Explicit or implicit commit is not allowed in stored function or trigger.
Bye

ERROR 1422 menolak COMMIT di dalam trigger — BEGIN, ROLLBACK, dan statement DDL yang meng-commit implisit ditolak dengan alasan sama. Aturan ini menjaga makna transaksi tetap utuh: aplikasi yang membuka transaksi, mengubah lima tabel, lalu ROLLBACK, berhak mendapatkan seluruh perubahan itu batal — termasuk efek trigger di dalamnya. Trigger boleh berpartisipasi dalam transaksi pemanggil, tetapi tidak boleh memutuskan nasibnya.

Rekursi Dilarang, Kedalaman Terbatas #

Trigger tidak boleh mengubah tabel tempat trigger itu sendiri menempel — pelanggarannya berujung ERROR 1442 yang sudah kita bedah tuntas di chapter debugging. Larangan ini memutus satu-satunya bentuk rekursi langsung yang paling mudah terjadi tanpa sengaja. Rantai trigger lintas tabel (trigger di tabel A menulis tabel B, trigger B menulis tabel A) secara teknis mungkin dan berpotensi berputar tanpa henti, sehingga desain trigger saling memanggil seperti itu harus dihindari sejak awal — MySQL tidak menawarkan penyangra bawaan yang bisa kita andalkan untuk pola tersebut. Prinsip praktisnya: satu trigger menulis ke tabel tujuan yang berbeda, dan selesai.

Trigger Berjalan Per Baris, Bukan Per Statement #

Klausa FOR EACH ROW bukan dekorasi. Statement UPDATE ... WHERE yang menyentuh sejuta baris menyalakan isi trigger sejuta kali — sekali per baris, berurutan. Konsekuensinya berganda: pekerjaan per baris harus sekecil mungkin (baca OLD/NEW, tulis satu log), query berat seperti join besar atau agregat di dalam trigger berlipat biayanya, dan alternatif satu statement (UPDATE massal dengan ekspresi, INSERT ... SELECT langsung) hampir selalu lebih cepat bila logikanya memungkinkan. MySQL tidak menyediakan trigger per statement yang menyala sekali untuk seluruh batch — semua trigger di MySQL bersemantik baris.

Batasan Sisi Event #

Event hidup di bawah scheduler, dan scheduler hanyalah thread di dalam server. Konsekuensinya sederhana namun sering dilupakan. Pertama, seluruh event berhenti ketika event_scheduler dimatikan — definisi event tetap tersimpan, hanya eksekusinya yang menunggu. Kedua, event tidak berjalan ketika server mati: event berkala akan lanjut pada interval berikutnya begitu server hidup, tetapi event sekali jalan yang waktunya terlewat tidak dikejar — dengan bawaan ON COMPLETION NOT PRESERVE, definisinya pun ikut terhapus, sehingga jadwal penting sebaiknya dijaga dengan alat pemantau, bukan sekadar event. Ketiga, satu event hanya membawa satu jadwal; kebutuhan “setiap hari pukul 2 dan setiap Minggu pukul 5” harus dipecah menjadi dua event terpisah. Sebagai objek database, event juga tunduk pada hak akses: pembuatnya perlu hak EVENT pada schema, dan definisinya terikat ke database tempat ia dibuat.

Tabel Ringkas Batasan #

BatasanJenisBukti / Akibat
Tidak boleh mengembalikan result setTriggerERROR 1415 saat CREATE maupun saat CALL procedure ber-result-set
Hanya untuk tabel dasarTriggerERROR 1347 untuk view; trigger lewat view diarahkan ke tabel dasar
Tanpa transaksi eksplisitTriggerERROR 1422 untuk COMMIT, BEGIN, ROLLBACK, DDL
Tidak boleh mengubah tabel sendiriTriggerERROR 1442; statement pemanggil di-rollback utuh
Berjalan per barisTriggerBiaya berlipat untuk statement massal
Butuh scheduler menyalaEventSET GLOBAL event_scheduler = ON sebelum event berjalan
Berhenti saat server matiEventJadwal terlewat; event sekali jalan tidak dikejar
Satu jadwal per eventEventJadwal ganda dipecah menjadi beberapa event

Panduan Penutup: Keep It Simple #

Seluruh section ini memperlihatkan trigger dan event yang menyelesaikan masalah nyata dengan elegan — dan seluruh batasan di atas memperlihatkan sisi harganya. Panduan praktis kita: pakai trigger untuk aturan bisnis yang memang milik data (validasi lintas tabel, audit ringan, counter turunan), pakai event untuk perawatan berkala yang terdefinisi rapi, dan jangan pernah membangun logika bisnis utama di dalam trigger. Alasannya bukan teknis semata: trigger sulit diuji satuan, sulit dibedah saat salah, dan efeknya menyebar diam-diam ke semua aplikasi yang menyentuh tabel. Kalau sebuah kebutuhan mulai menuntut trigger yang memanggil procedure yang memicu trigger lain, pindahkan orkestrasinya ke procedure yang dipanggil eksplisit atau ke lapisan aplikasi — kode yang bisa dilihat, diuji, dan diberi versi.

Membersihkan Database Latihan #

Procedure, view, trigger yang tersisa, dan seluruh tabel latihan kita jatuhkan bersama:

sql
DROP DATABASE belajar_trig;

Output yang Dihasilkan:

text
Query OK, 3 rows affected (0.00 sec)

Referensi #

Trigger dan event melengkapi peta kemampuan server-side MySQL: dari stored procedure yang dipanggil eksplisit, trigger yang menyala otomatis, hingga event yang berjalan terjadwal. Dengan batasan-batasannya yang kini teruji, kita bisa memilih alat yang tepat tanpa mencoba memaksakan yang salah. Analisis data kita pun bergerak ke ranah berikutnya: selama ini GROUP BY merangkum baris menjadi kelompok, padahal banyak pertanyaan — seperti peringkat, selisih antarbaris, dan agregat berjendela — butuh melihat baris-baris tetap berdampingan. Section window functions membuka kemampuan itu.