Foto oleh panumas nikhomkhai via Pexels

Trigger adalah blok kode yang dijalankan otomatis oleh SQL Server ketika sebuah event tertentu terjadi pada tabel, seperti INSERT, UPDATE, atau DELETE. Berbeda dengan stored procedure yang harus dipanggil secara eksplisit, trigger berjalan sendiri di belakang layar tanpa perlu dipanggil manual oleh aplikasi.

Trigger paling sering dipakai untuk kebutuhan audit (mencatat siapa dan kapan sebuah data diubah), validasi tambahan yang terlalu kompleks untuk CHECK constraint biasa, atau menjaga konsistensi data antar tabel yang saling terkait tanpa mengandalkan aplikasi untuk selalu melakukannya dengan benar.

Meski berguna, trigger juga punya reputasi sebagai sumber bug yang sulit dilacak karena sifatnya yang tersembunyi dari alur kode aplikasi. Artikel ini membahas cara membuat trigger dasar, memahami tabel virtual inserted dan deleted, serta kapan sebaiknya trigger dihindari.

Trigger AFTER INSERT untuk Audit Log

Trigger AFTER INSERT mencatat log otomatis

CREATE TRIGGER trg_LogInsertOrder ON Orders AFTER INSERT AS BEGIN INSERT INTO OrderAuditLog (OrderID, Aksi, WaktuAksi) SELECT OrderID, 'INSERT', GETDATE() FROM inserted; END; -- Expected: setiap kali ada baris baru di Orders, -- OrderAuditLog otomatis mencatatnya tanpa kode tambahan di aplikasi
Trigger AFTER INSERT dijalankan setelah baris baru berhasil dimasukkan ke tabel, dan tabel virtual bernama 'inserted' berisi salinan baris-baris yang baru saja ditambahkan tersebut. Dengan membaca tabel inserted, trigger bisa mencatat informasi tambahan ke tabel log tanpa perlu mengubah kode aplikasi yang melakukan proses insert awal.

Memahami Tabel Virtual inserted dan deleted

UPDATE memakai kedua tabel virtual sekaligus

CREATE TRIGGER trg_LogUpdateHarga ON Products AFTER UPDATE AS BEGIN INSERT INTO PriceChangeLog (ProductID, HargaLama, HargaBaru, WaktuUbah) SELECT d.ProductID, d.Price, i.Price, GETDATE() FROM deleted d JOIN inserted i ON d.ProductID = i.ProductID WHERE d.Price <> i.Price; END;
Pada trigger UPDATE, tabel 'deleted' berisi nilai baris sebelum diubah dan tabel 'inserted' berisi nilai baris setelah diubah, sehingga menggabungkan keduanya dengan JOIN memungkinkan trigger mengetahui persis kolom mana yang berubah dan apa nilai lama maupun barunya. Pola ini sangat umum dipakai untuk audit trail yang mencatat histori perubahan data.

Trigger INSTEAD OF untuk Mengganti Perilaku Default

INSTEAD OF mengganti perilaku DELETE default

CREATE TRIGGER trg_CegahHapusPesananSelesai ON Orders INSTEAD OF DELETE AS BEGIN IF EXISTS (SELECT 1 FROM deleted WHERE Status = 'Completed') BEGIN RAISERROR('Pesanan yang sudah selesai tidak boleh dihapus', 16, 1); RETURN; END DELETE FROM Orders WHERE OrderID IN (SELECT OrderID FROM deleted WHERE Status <> 'Completed'); END;
Trigger INSTEAD OF berjalan sebagai pengganti operasi aslinya, bukan sesudahnya, sehingga trigger tersebut yang menentukan apa yang sebenarnya terjadi terhadap data. Pada contoh ini, perintah DELETE biasa tidak langsung menghapus baris, melainkan dicegat oleh trigger yang memvalidasi terlebih dahulu apakah pesanan berstatus Completed sebelum mengizinkan penghapusan.

Menonaktifkan dan Mengaktifkan Kembali Trigger

DISABLE dan ENABLE TRIGGER untuk kebutuhan sementara

-- Menonaktifkan sementara, misalnya untuk proses migrasi data massal DISABLE TRIGGER trg_LogInsertOrder ON Orders; -- Lakukan proses bulk insert di sini... -- Aktifkan kembali setelah selesai ENABLE TRIGGER trg_LogInsertOrder ON Orders;
Ada kalanya trigger perlu dinonaktifkan sementara, misalnya saat melakukan migrasi data dalam jumlah besar yang tidak memerlukan proses audit setiap barisnya, karena trigger yang berjalan untuk setiap baris pada operasi bulk bisa sangat memperlambat proses tersebut. DISABLE TRIGGER memungkinkan ini dilakukan tanpa harus menghapus definisi triggernya sama sekali.

INSTEAD OF Trigger vs AFTER Trigger dan Kasus Penggunaannya

AFTER trigger mencatat audit, INSTEAD OF trigger mengubah perilaku DELETE pada view

-- AFTER trigger: berjalan setelah data benar-benar tersimpan CREATE TRIGGER trg_Orders_AfterInsert ON Orders AFTER INSERT AS BEGIN INSERT INTO AuditLog (TableName, Action, RecordID, ActionDate) SELECT 'Orders', 'INSERT', OrderID, GETDATE() FROM inserted; END; -- INSTEAD OF trigger: menggantikan operasi asli, berguna pada view CREATE TRIGGER trg_vw_ActiveCustomers_InsteadOfDelete ON dbo.vw_ActiveCustomers INSTEAD OF DELETE AS BEGIN UPDATE Customers SET IsActive = 0 WHERE CustomerID IN (SELECT CustomerID FROM deleted); END; -- DELETE FROM vw_ActiveCustomers WHERE CustomerID = 10 akan soft-delete, bukan menghapus baris

AFTER trigger (juga disebut FOR trigger) dieksekusi setelah operasi DML yang memicunya berhasil diterapkan ke tabel, sehingga tabel inserted dan deleted yang tersedia di dalam trigger sudah mencerminkan kondisi data pasca-perubahan; jenis trigger ini hanya bisa dipasang pada tabel, bukan pada view, dan umum dipakai untuk audit trail, validasi lanjutan, atau sinkronisasi ke tabel lain.

INSTEAD OF trigger justru menggantikan sepenuhnya operasi DML asli dengan logika kustom di dalam badan trigger, sehingga pernyataan INSERT, UPDATE, atau DELETE yang memicunya tidak pernah benar-benar dieksekusi terhadap objek target kecuali ditulis ulang secara eksplisit di dalam trigger tersebut. Kemampuan ini membuat INSTEAD OF trigger menjadi satu-satunya cara membuat view yang melibatkan banyak tabel tetap bisa menerima operasi DML secara langsung.

Penggunaan trigger secara berlebihan dapat membuat alur data sulit ditelusuri karena perubahan pada satu tabel bisa memicu efek samping tersembunyi di tabel lain tanpa terlihat jelas pada kode aplikasi, sehingga praktik umum adalah membatasi trigger hanya untuk kebutuhan yang benar-benar tidak bisa dipenuhi di lapisan aplikasi, seperti audit wajib atau enforcement aturan integritas lintas tabel yang kompleks.

Risiko Recursive Trigger dan Cara Mengontrolnya

RECURSIVE_TRIGGERS OFF mencegah trigger memicu dirinya sendiri secara langsung

-- Trigger yang berpotensi memicu dirinya sendiri secara rekursif CREATE TRIGGER trg_Employees_SyncManagerCount ON Employees AFTER UPDATE AS BEGIN IF UPDATE(ManagerID) BEGIN UPDATE Employees SET TeamSize = (SELECT COUNT(*) FROM Employees e2 WHERE e2.ManagerID = Employees.EmployeeID) WHERE EmployeeID IN (SELECT ManagerID FROM inserted); -- UPDATE di atas MEMICU trigger ini lagi jika RECURSIVE_TRIGGERS ON END END; -- Memeriksa dan menonaktifkan rekursi trigger di level database SELECT name, is_recursive_triggers_on FROM sys.databases WHERE name = DB_NAME(); ALTER DATABASE CURRENT SET RECURSIVE_TRIGGERS OFF; -- Dengan OFF, trigger tidak memicu dirinya sendiri secara langsung (direct recursion)

Recursive trigger terjadi ketika sebuah trigger melakukan operasi DML yang kembali memicu trigger yang sama (direct recursion) atau memicu trigger pada tabel lain yang pada akhirnya mengubah tabel asal (indirect recursion). Pada contoh di atas, trigger AFTER UPDATE pada Employees melakukan UPDATE lagi ke tabel Employees untuk menyinkronkan TeamSize, yang berpotensi memicu dirinya sendiri berulang kali jika opsi rekursi diaktifkan.

SQL Server menyediakan pengaturan database RECURSIVE_TRIGGERS yang secara default OFF, sehingga direct recursion dicegah secara otomatis; namun indirect recursion, yaitu trigger pada tabel A memicu trigger pada tabel B yang kemudian mengubah tabel A lagi, tetap bisa terjadi terlepas dari pengaturan RECURSIVE_TRIGGERS karena pengaturan tersebut hanya mengontrol rekursi langsung pada tabel yang sama. Rantai indirect recursion semacam ini jauh lebih sulit dideteksi karena melibatkan beberapa trigger pada tabel berbeda yang saling terhubung secara tidak langsung.

Untuk mencegah rekursi tak terkendali maupun efek samping berantai yang sulit ditelusuri, praktik yang disarankan adalah menambahkan pengecekan kondisi di awal trigger menggunakan fungsi TRIGGER_NESTLEVEL() untuk mendeteksi apakah trigger sedang dipanggil dari dalam trigger lain, atau merancang ulang logika agar perhitungan seperti TeamSize dilakukan melalui view/computed column daripada menulis ulang tabel yang sama di dalam triggernya sendiri. SQL Server juga membatasi kedalaman nested trigger hingga maksimum 32 level, dan trigger yang melampaui batas ini akan dibatalkan seluruhnya oleh mesin database.

DDL Trigger untuk Mengaudit Perubahan Skema Database

DDL trigger di level DATABASE merekam setiap perubahan struktur tabel

-- DDL trigger menangkap perubahan struktur objek, bukan perubahan data CREATE TRIGGER trg_AuditSchemaChanges ON DATABASE FOR CREATE_TABLE, ALTER_TABLE, DROP_TABLE AS BEGIN DECLARE @EventData XML = EVENTDATA(); INSERT INTO SchemaAuditLog (EventType, ObjectName, LoginName, EventDate, EventXML) VALUES ( @EventData.value('(/EVENT_INSTANCE/EventType)[1]', 'VARCHAR(50)'), @EventData.value('(/EVENT_INSTANCE/ObjectName)[1]', 'VARCHAR(200)'), @EventData.value('(/EVENT_INSTANCE/LoginName)[1]', 'VARCHAR(200)'), GETDATE(), @EventData ); END; -- Setiap CREATE/ALTER/DROP TABLE di database ini otomatis tercatat ke SchemaAuditLog

DDL trigger berbeda dari DML trigger karena dipicu oleh pernyataan yang mengubah struktur objek seperti CREATE TABLE, ALTER TABLE, atau DROP TABLE, bukan oleh perubahan data melalui INSERT, UPDATE, atau DELETE. Trigger jenis ini bisa dipasang pada level DATABASE untuk memantau objek di satu database, atau level SERVER untuk memantau perubahan lintas database.

Fungsi EVENTDATA() mengembalikan dokumen XML yang berisi detail lengkap peristiwa DDL yang memicu trigger, termasuk jenis perintah, nama objek yang terpengaruh, dan login yang menjalankannya, sehingga informasi tersebut bisa diekstrak dan disimpan ke tabel audit. Penggunaan umumnya adalah mencegah perubahan skema tanpa persetujuan atau sekadar menjaga jejak audit perubahan struktur database dari waktu ke waktu. DDL trigger juga bisa dipakai bersama ROLLBACK di dalam badan trigger untuk benar-benar memblokir pernyataan DDL tertentu, misalnya menolak DROP TABLE pada jam kerja kecuali dijalankan oleh akun administratif tertentu.

Kesalahan Umum yang Sering Terjadi

Menulis Logika Bisnis Kompleks di Dalam Trigger

Trigger yang berisi logika bisnis rumit membuat alur aplikasi menjadi tidak transparan, karena developer yang membaca kode aplikasi tidak akan tahu ada proses tambahan yang berjalan otomatis di level database kecuali memeriksa trigger secara terpisah.

Lupa Trigger Berjalan per Statement, Bukan per Baris

Trigger di SQL Server berjalan satu kali untuk seluruh statement meski statement tersebut memengaruhi banyak baris sekaligus, sehingga kode yang mengasumsikan hanya satu baris di tabel inserted/deleted akan salah ketika terjadi bulk insert atau bulk update.

Membuat Trigger yang Memanggil Trigger Lain Secara Berantai

Trigger pada satu tabel yang mengubah tabel lain yang juga punya trigger bisa memicu rantai trigger yang sulit dilacak dan berpotensi menyebabkan infinite loop jika tidak dirancang dengan hati-hati.

Kapan Pendekatan Ini Tidak Cocok Dipakai

Trigger sangat berguna untuk memastikan aturan tertentu selalu berlaku tanpa bergantung pada disiplin setiap developer aplikasi untuk mengingatnya, seperti audit log yang harus selalu tercatat apapun cara data diubah.

Namun karena sifatnya yang tersembunyi dan sulit di-debug, banyak tim memilih memindahkan logika yang bisa dipindahkan ke layer aplikasi atau ke stored procedure yang dipanggil secara eksplisit, menyisakan trigger hanya untuk kebutuhan yang benar-benar harus dijamin konsisten di level database.

Ringkasan

Trigger memberi cara untuk menjalankan logika otomatis saat data berubah, dengan tabel virtual inserted dan deleted sebagai kunci untuk mengetahui data sebelum dan sesudah perubahan. Gunakan trigger secukupnya untuk kebutuhan yang memang harus dijamin konsisten di level database, dan hindari menumpuk logika bisnis kompleks di dalamnya.