← Kursa Dön
📄 Text · 30 min

Triggers — Otomatik Tetiklenen İşlemler

Giriş — "Her Sipariş Oluşturulduğunda Stoku Otomatik Düş"

Bir e-ticaret sisteminde sipariş oluşturulduğunda birçok şeyin otomatik olması gerekir: stok düşmeli, müşterinin sipariş sayısı güncellenmeli, belki bir log kaydı tutulmalı. Bu işlemleri uygulama kodunda yapmak mümkün ama riskli — birisi doğrudan veritabanına bağlanıp INSERT yaparsa, uygulama kodundaki mantık çalışmaz.

Trigger, bir tablo üzerinde INSERT, UPDATE veya DELETE işlemi gerçekleştiğinde otomatik olarak çalışan bir SQL programıdır. Uygulama katmanından bağımsızdır — veri nereden gelirse gelsin, trigger tetiklenir.

🎯 Analoji: Trigger'ı bir evin alarm sistemi gibi düşün. Kapı açıldığında (INSERT), pencere kırıldığında (UPDATE), bir eşya çıkarıldığında (DELETE) alarm otomatik çalışır. Kim kapıyı açarsa açsın — ev sahibi, hırsız, tamirci — alarm her durumda devreye girer. Trigger da aynı: veri kim tarafından değiştirilirse değiştirilsin, otomatik çalışır.


Trigger Temelleri

Syntax

CREATE TRIGGER trigger_adı
{BEFORE | AFTER} {INSERT | UPDATE | DELETE}
ON tablo_adı
FOR EACH ROW
BEGIN
    -- Tetiklenecek işlemler
END;

Bir trigger tanımlarken 3 karar vermen gerekir:

  1. Zamanlama: BEFORE (işlemden önce) veya AFTER (işlemden sonra)

  2. Olay: INSERT, UPDATE veya DELETE

  3. Tablo: Hangi tablo üzerinde?

NEW ve OLD — Eski ve Yeni Değerler

Trigger içinde iki özel kelime var:

  • NEW: INSERT veya UPDATE'deki yeni değerler

  • OLD: UPDATE veya DELETE'deki eski değerler

İşlemOLDNEW
INSERTYok✅ Eklenen satır
UPDATE✅ Eski değerler✅ Yeni değerler
DELETE✅ Silinen satırYok

AFTER INSERT Trigger — En Yaygın Kullanım

Sipariş Log'u

DELIMITER //

CREATE TRIGGER after_order_insert
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
    INSERT INTO order_log (order_id, action, action_date, details)
    VALUES (
        NEW.order_id, 
        'INSERT', 
        NOW(), 
        CONCAT('Yeni sipariş: müşteri=', NEW.customer_id, 
               ', tutar=', NEW.total_amount)
    );
END //

DELIMITER ;

Artık orders tablosuna her INSERT yapıldığında, order_log tablosuna otomatik kayıt oluşturulur. Uygulama kodunu değiştirmene gerek yok.

Stok Güncelleme

DELIMITER //

CREATE TRIGGER after_order_item_insert
AFTER INSERT ON order_items
FOR EACH ROW
BEGIN
    -- Stoku otomatik düş
    UPDATE products 
    SET stock_quantity = stock_quantity - NEW.quantity
    WHERE product_id = NEW.product_id;
    
    -- Stok uyarısı log'u
    INSERT INTO stock_alerts (product_id, alert_type, alert_date, details)
    SELECT 
        NEW.product_id, 
        'LOW_STOCK', 
        NOW(),
        CONCAT('Kalan stok: ', stock_quantity - NEW.quantity)
    FROM products
    WHERE product_id = NEW.product_id
      AND stock_quantity - NEW.quantity < 10;  -- Stok 10'un altına düşerse
END //

DELIMITER ;

BEFORE INSERT Trigger — Veri Doğrulama ve Otomatik Değer Atama

BEFORE trigger'lar, veri tabloya yazılmadan önce çalışır. Doğrulama, varsayılan değer atama ve veri dönüştürme için idealdir.

DELIMITER //

CREATE TRIGGER before_customer_insert
BEFORE INSERT ON customers
FOR EACH ROW
BEGIN
    -- E-posta küçük harfe çevir
    SET NEW.email = LOWER(TRIM(NEW.email));
    
    -- Telefon numarasını formatla
    SET NEW.phone = REPLACE(REPLACE(REPLACE(NEW.phone, ' ', ''), '-', ''), '(', '');
    SET NEW.phone = REPLACE(NEW.phone, ')', '');
    
    -- Kayıt tarihini otomatik ata
    IF NEW.registration_date IS NULL THEN
        SET NEW.registration_date = NOW();
    END IF;
    
    -- Geçersiz e-posta kontrolü
    IF NEW.email NOT LIKE '%@%.%' THEN
        SIGNAL SQLSTATE '45000' 
            SET MESSAGE_TEXT = 'Geçersiz e-posta adresi';
    END IF;
END //

DELIMITER ;

BEFORE INSERT'te NEW.sütun = değer ile veriyi değiştirebilirsin — veri tabloya bu değiştirilmiş haliyle yazılır. AFTER INSERT'te NEW salt okunurdur.

Fiyat Doğrulama

DELIMITER //

CREATE TRIGGER before_product_insert
BEFORE INSERT ON products
FOR EACH ROW
BEGIN
    -- Negatif fiyat engelle
    IF NEW.price < 0 THEN
        SIGNAL SQLSTATE '45000' 
            SET MESSAGE_TEXT = 'Ürün fiyatı negatif olamaz';
    END IF;
    
    -- Maksimum fiyat kontrolü
    IF NEW.price > 1000000 THEN
        SIGNAL SQLSTATE '45000' 
            SET MESSAGE_TEXT = 'Ürün fiyatı 1.000.000 TL üzerinde olamaz';
    END IF;
    
    -- Stok varsayılan değeri
    IF NEW.stock_quantity IS NULL THEN
        SET NEW.stock_quantity = 0;
    END IF;
END //

DELIMITER ;

UPDATE Trigger — Değişiklik İzleme (Audit Trail)

DELIMITER //

CREATE TRIGGER after_product_update
AFTER UPDATE ON products
FOR EACH ROW
BEGIN
    -- Fiyat değişikliği log'u
    IF OLD.price != NEW.price THEN
        INSERT INTO price_history (
            product_id, old_price, new_price, change_date, change_pct
        ) VALUES (
            NEW.product_id, 
            OLD.price, 
            NEW.price, 
            NOW(),
            ROUND((NEW.price - OLD.price) * 100.0 / OLD.price, 2)
        );
    END IF;
    
    -- Stok değişikliği log'u
    IF OLD.stock_quantity != NEW.stock_quantity THEN
        INSERT INTO stock_history (
            product_id, old_quantity, new_quantity, change_date
        ) VALUES (
            NEW.product_id,
            OLD.stock_quantity,
            NEW.stock_quantity,
            NOW()
        );
    END IF;
END //

DELIMITER ;

Bu trigger sayesinde ürün fiyatı veya stoku her değiştiğinde otomatik log tutulur. "Bu ürünün fiyatı ne zaman, ne kadar değişti?" sorusuna anında cevap verebilirsin.

BEFORE UPDATE — Değişikliği Engelleme veya Modifiye Etme

DELIMITER //

CREATE TRIGGER before_order_update
BEFORE UPDATE ON orders
FOR EACH ROW
BEGIN
    -- Tamamlanmış siparişin statusu değiştirilemez
    IF OLD.status = 'completed' AND NEW.status != 'completed' THEN
        SIGNAL SQLSTATE '45000' 
            SET MESSAGE_TEXT = 'Tamamlanmış sipariş durumu değiştirilemez';
    END IF;
    
    -- İptal edilen siparişte tutar sıfırlanır
    IF NEW.status = 'cancelled' AND OLD.status != 'cancelled' THEN
        SET NEW.total_amount = 0;
    END IF;
    
    -- Son güncelleme tarihini otomatik ayarla
    SET NEW.updated_at = NOW();
END //

DELIMITER ;

DELETE Trigger — Silme İşlemi İzleme

DELIMITER //

CREATE TRIGGER before_customer_delete
BEFORE DELETE ON customers
FOR EACH ROW
BEGIN
    -- Aktif siparişi olan müşteri silinemez
    DECLARE v_active_orders INT;
    
    SELECT COUNT(*) INTO v_active_orders
    FROM orders 
    WHERE customer_id = OLD.customer_id 
      AND status IN ('pending', 'processing', 'shipped');
    
    IF v_active_orders > 0 THEN
        SIGNAL SQLSTATE '45000' 
            SET MESSAGE_TEXT = 'Aktif siparişi olan müşteri silinemez';
    END IF;
    
    -- Silinen müşteri bilgisini arşivle
    INSERT INTO deleted_customers_archive (
        customer_id, first_name, last_name, email, deleted_at
    ) VALUES (
        OLD.customer_id, OLD.first_name, OLD.last_name, OLD.email, NOW()
    );
END //

DELIMITER ;

Sipariş Silme — Stok Geri Yükleme

DELIMITER //

CREATE TRIGGER after_order_item_delete
AFTER DELETE ON order_items
FOR EACH ROW
BEGIN
    -- Silinen sipariş kaleminin stokunu geri yükle
    UPDATE products 
    SET stock_quantity = stock_quantity + OLD.quantity
    WHERE product_id = OLD.product_id;
END //

DELIMITER ;

Trigger Zinciri ve Dikkat Noktaları

Trigger Sırası (MySQL 5.7.2+)

Aynı tablo ve olay için birden fazla trigger varsa, sıra belirtilebilir:

CREATE TRIGGER trigger_1
BEFORE INSERT ON orders
FOR EACH ROW
FOLLOWS trigger_0  -- trigger_0'dan sonra çalış
BEGIN
    ...
END;

CREATE TRIGGER trigger_2
BEFORE INSERT ON orders
FOR EACH ROW
PRECEDES trigger_1  -- trigger_1'den önce çalış
BEGIN
    ...
END;

Trigger İçinde Trigger (Cascading)

-- Trigger 1: orders'a INSERT → order_log'a INSERT
-- Trigger 2: order_log'a INSERT → notification'a INSERT
-- Bu zincirleme (cascading) trigger — dikkatli ol!

⚠️ Dikkat: Cascading trigger'lar debug etmesi zor hatalara neden olabilir. Trigger A, trigger B'yi tetikler, o da trigger C'yi tetikler... Hangi trigger ne yaptı bulmak kabus olur. Mümkünse trigger zincirlerini kısa tut.

Trigger Performans Etkisi

-- Her INSERT'te trigger çalışır — toplu INSERT'lerde dikkat!

-- ❌ 100.000 satırlık bulk INSERT — her satır için trigger çalışır
INSERT INTO order_items (order_id, product_id, quantity, unit_price)
SELECT ... FROM temp_import;  -- 100.000 kez trigger tetiklenir!

-- Çözümler:
-- 1. Trigger'ı geçici olarak devre dışı bırak
-- MySQL'de doğrudan DISABLE TRIGGER yok ama:
-- SET @TRIGGER_DISABLED = 1;  -- Trigger içinde kontrol et

-- 2. Bulk işlem sonrası toplu güncelleme yap

Trigger Yönetimi

-- Trigger'ları listele
SHOW TRIGGERS FROM ecommerce;

-- Belirli tablonun trigger'ları
SHOW TRIGGERS FROM ecommerce WHERE `Table` = 'orders';

-- Trigger tanımını göster
SHOW CREATE TRIGGER after_order_insert;

-- Trigger'ı sil
DROP TRIGGER IF EXISTS after_order_insert;

-- MySQL'de ALTER TRIGGER yok — sil ve yeniden oluştur

Gerçek Dünya Örneği — Tam Audit Sistemi

-- Genel audit log tablosu
CREATE TABLE audit_log (
    log_id BIGINT AUTO_INCREMENT PRIMARY KEY,
    table_name VARCHAR(100),
    action ENUM('INSERT', 'UPDATE', 'DELETE'),
    record_id INT,
    old_values JSON,
    new_values JSON,
    changed_by VARCHAR(100),
    changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- Orders tablosu için tam audit trigger'ı
DELIMITER //

CREATE TRIGGER orders_audit_insert
AFTER INSERT ON orders FOR EACH ROW
BEGIN
    INSERT INTO audit_log (table_name, action, record_id, new_values, changed_by)
    VALUES ('orders', 'INSERT', NEW.order_id,
        JSON_OBJECT(
            'customer_id', NEW.customer_id,
            'total_amount', NEW.total_amount,
            'status', NEW.status
        ),
        CURRENT_USER()
    );
END //

CREATE TRIGGER orders_audit_update
AFTER UPDATE ON orders FOR EACH ROW
BEGIN
    INSERT INTO audit_log (table_name, action, record_id, old_values, new_values, changed_by)
    VALUES ('orders', 'UPDATE', NEW.order_id,
        JSON_OBJECT(
            'total_amount', OLD.total_amount,
            'status', OLD.status
        ),
        JSON_OBJECT(
            'total_amount', NEW.total_amount,
            'status', NEW.status
        ),
        CURRENT_USER()
    );
END //

CREATE TRIGGER orders_audit_delete
AFTER DELETE ON orders FOR EACH ROW
BEGIN
    INSERT INTO audit_log (table_name, action, record_id, old_values, changed_by)
    VALUES ('orders', 'DELETE', OLD.order_id,
        JSON_OBJECT(
            'customer_id', OLD.customer_id,
            'total_amount', OLD.total_amount,
            'status', OLD.status
        ),
        CURRENT_USER()
    );
END //

DELIMITER ;

Bu audit sistemi, orders tablosundaki her değişikliği JSON formatında kaydeder — kim, ne zaman, ne değiştirdi, eski ve yeni değerler neydi.


Ne Zaman Trigger Kullanmalı, Ne Zaman Kullanmamalı?

✅ Kullan

  • Audit log / değişiklik takibi — Kim ne değiştirdi?

  • Veri doğrulama — BEFORE trigger ile geçersiz veriyi engelle

  • Otomatik değer atama — created_at, updated_at gibi

  • Denormalizasyon güncelleme — Özet tabloları otomatik güncelle

  • Referans bütünlüğü — FK constraint'in yetersiz kaldığı durumlar

❌ Kullanma

  • Karmaşık iş mantığı — Uygulama katmanında yap, test edilebilir olsun

  • Harici servis çağrıları — E-posta gönderme, API çağrısı trigger'dan yapılmaz

  • Performans kritik toplu işlemler — Trigger her satırda çalışır, toplu işlemleri yavaşlatır

  • Zincirleme trigger'lar — Debug etmesi imkansız hale gelir


PostgreSQL Farklılıkları

-- PostgreSQL: Trigger function ayrı tanımlanır
CREATE OR REPLACE FUNCTION orders_audit_func()
RETURNS TRIGGER AS $$
BEGIN
    IF TG_OP = 'INSERT' THEN
        INSERT INTO audit_log (table_name, action, record_id, new_values)
        VALUES ('orders', 'INSERT', NEW.order_id, row_to_json(NEW));
    ELSIF TG_OP = 'UPDATE' THEN
        INSERT INTO audit_log (table_name, action, record_id, old_values, new_values)
        VALUES ('orders', 'UPDATE', NEW.order_id, row_to_json(OLD), row_to_json(NEW));
    ELSIF TG_OP = 'DELETE' THEN
        INSERT INTO audit_log (table_name, action, record_id, old_values)
        VALUES ('orders', 'DELETE', OLD.order_id, row_to_json(OLD));
    END IF;
    RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;

-- Tek trigger function, üç olay:
CREATE TRIGGER orders_audit
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW EXECUTE FUNCTION orders_audit_func();
-- MySQL'de tek trigger'da birden fazla olay desteklenmez

-- PostgreSQL: Statement-level trigger (tablo bazlı, satır bazlı değil)
CREATE TRIGGER after_bulk_insert
AFTER INSERT ON orders
FOR EACH STATEMENT EXECUTE FUNCTION notify_admin();
-- MySQL'de FOR EACH STATEMENT yok — sadece FOR EACH ROW

Özet

  • Trigger bir tablo üzerinde INSERT/UPDATE/DELETE olduğunda otomatik çalışan SQL programıdır

  • BEFORE trigger: veri yazılmadan önce — doğrulama, dönüştürme, engelleme

  • AFTER trigger: veri yazıldıktan sonra — log tutma, bağımlı tablo güncelleme

  • NEW: eklenen/güncellenen yeni değerler, OLD: güncellenen/silinen eski değerler

  • SIGNAL ile özel hata fırlatarak işlemi engelleyebilirsin

  • Audit trail (değişiklik takibi) için en güçlü araç

  • Trigger her satır için çalışır — toplu işlemlerde performansı etkiler

Sıkça Yapılan Hatalar

  1. Trigger içinde aynı tabloyu güncellemek — MySQL'de trigger, tetikleyen tabloda DML yapamaz. BEFORE trigger'da SET NEW.sütun = değer kullan.

  2. Cascading trigger'lar oluşturmak — Trigger A → trigger B → trigger C zinciri debug kabusudur. Mümkünse doğrudan yaz.

  3. Trigger'ı performans testinden geçirmemek — Trigger her satırda çalışır. 100.000 satırlık bulk INSERT'te 100.000 kez tetiklenir. Test et!

  4. SIGNAL'ı BEFORE yerine AFTER'da kullanmak — AFTER'da SIGNAL fırlatmak işlemi geri alır ama veri zaten yazılmış olabilir. Doğrulama her zaman BEFORE'da yap.

  5. Trigger'ların varlığını unutmak — Aylar sonra "bu log kaydı nereden geliyor?" diye sorgularken, trigger'ların varlığını hatırlayamayan geliştiriciler çok yaygın. Trigger'ları belgele.