MySQL Tetikleyicileri ve Uygulama Mantığı Tercihi

Merhabalar, bu yazımda klasik bir tartışmaya bakacağız: MySQL trigger'larını mı kullanmalıyız, yoksa aynı işi uygulama katmanında mı halletmeliyiz? Cevap 'duruma göre değişir' tabii, ama duruma göre değişen şey ne, onu netleştirmeye çalışacağım. Hadi başlayalım.

Bu soru özellikle bir ekibe yeni katıldığınızda karşınıza çıkar. Bir bakarsınız audit kayıtları sihirli bir şekilde doluyor; sonra fark edersiniz ki kimsenin haberi olmadan bir trigger arka planda iş yapıyor. Tersi de geçerli: tüm mantık servislerde, ama biri direkt SQL ile veritabanına dokunduğunda invariant'lar uçuyor.

Trigger nedir, hatırlatma

Trigger (tetikleyici), belli bir tablodaki INSERT, UPDATE veya DELETE işlemleri öncesinde ya da sonrasında otomatik çalışan, aynı transaction'a bağlı küçük bir prosedürdür. Klasik kullanım denetim kaydı (audit log) tutmaktır. Hadi bir örnek görelim:

CREATE TABLE employee_audit (
    id BIGINT AUTO_INCREMENT PRIMARY KEY,
    employee_id BIGINT NOT NULL,
    old_salary DECIMAL(10,2),
    new_salary DECIMAL(10,2),
    changed_by VARCHAR(100),
    changed_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

DELIMITER $$
CREATE TRIGGER trg_employee_salary_audit
AFTER UPDATE ON employees
FOR EACH ROW
BEGIN
    IF OLD.salary <> NEW.salary THEN
        INSERT INTO employee_audit (employee_id, old_salary, new_salary, changed_by)
        VALUES (NEW.id, OLD.salary, NEW.salary, CURRENT_USER());
    END IF;
END$$
DELIMITER ;

Buradaki güzellik şu: maaşı kim güncellerse güncellesin (web app, admin paneli, geceki migration, hatta DBA'nın mysql shell'i), kayıt otomatik düşer. Hiçbir client unutamaz, çünkü kararı veren veritabanı.

Trigger'ın güçlü olduğu yerler

  • Garantili çalışma: Birden fazla servis aynı tabloya yazıyorsa, kuralı tek noktada toplamak inanılmaz rahatlatır. Yarın yeni bir mikroservis eklediğinizde 'audit'i unutma' demek zorunda kalmazsınız.
  • Transactional güvenlik: Trigger fail olursa tüm işlem rollback olur. Yani 'maaş değişti ama audit yazılmadı' gibi tutarsız bir ara durum oluşmaz.
  • Veri bütünlüğü (integrity): Karmaşık invariant'ları (örneğin 'stok asla negatif olmamalı') BEFORE trigger ile ya da CHECK constraint ile burada kapatmak mantıklı.

Trigger'ın canınızı yakacağı yerler

Açıkçası ben de ilk öğrendiğimde trigger'ları çok cazip bulmuştum. Ama production'da birkaç sefer canım yandıktan sonra mesafeyi öğrendim:

  • Görünmez davranış: Yeni gelen developer kodu okur, basit bir UPDATE görür, arka planda üç tablo daha yazıldığını fark etmez. Debug saatlerce sürer.
  • Test zorluğu: Trigger'ı mock'layamazsınız, gerçek bir MySQL instance gerekir. Unit test yazma alışkanlığınız bozulur.
  • Performans: Bulk işlemlerde trigger satır başına çalışır. 100 bin satırlık bir update'te aradaki fark dramatik olabilir.
  • Sınırlı dil: MySQL'in prosedürel SQL'i, gerçek bir programlama dilinin yerini tutmaz. HTTP çağrısı yapamaz, dış servise mesaj atamaz.

Uygulama katmanında mantık

Aynı audit'i Python'da yazsak şöyle bir şey olurdu:

def update_salary(employee_id: int, new_salary: Decimal, actor: str) -> None:
    with db.transaction() as tx:
        old_salary = tx.fetch_value(
            'SELECT salary FROM employees WHERE id = %s FOR UPDATE',
            (employee_id,),
        )
        if old_salary == new_salary:
            return
        tx.execute(
            'UPDATE employees SET salary = %s WHERE id = %s',
            (new_salary, employee_id),
        )
        tx.execute(
            'INSERT INTO employee_audit (employee_id, old_salary, new_salary, changed_by) '
            'VALUES (%s, %s, %s, %s)',
            (employee_id, old_salary, new_salary, actor),
        )

Burada her şey gözünüzün önünde. Test edersiniz, version control'de izlersiniz, debug ederken stack trace açıktır. Tek sorun: yarın biri mysql shell'inden UPDATE employees SET salary = ... çekerse audit yazılmaz. Disiplin gerektirir.

Kısa karar matrisi

Senaryo Tercih
Çoklu client için audit log Trigger
Veri bütünlüğü invariant'ı Trigger veya CHECK constraint
Karmaşık iş akışı, dış servis çağrısı Uygulama
Servisler arası koordinasyon Uygulama (event/queue)
Büyük migration Uygulama (trigger'ı geçici devre dışı bırak)
Hesaplanmış sütun Generated column

Pratikte ne yapıyorum?

Bana sorarsanız, çoğu sistemde ikisi birden olmalı. Trigger'ı dar ve net bir kapsamda kullanın: audit ve veri bütünlüğü. İş mantığı, workflow, dış entegrasyon - bunların hepsi uygulama tarafında kalsın. Trigger'ı 'akıllı' yapmaya başladığınız an, ileride o kararın bedelini ödersiniz; ben ödedim, biliyorum.

Bir de şunu unutmayın: bulk işlemlerde trigger'lar performansı yere serebilir. Migration'larınızda gerekirse SET @TRIGGER_DISABLED = 1 benzeri bir flag ile trigger'ı devre dışı bırakacak şekilde tasarlayın, sonra tek seferlik audit'i toplu insert ile yazın.

Kapanış

Bu yazımızda MySQL trigger'larını uygulama mantığıyla karşılaştırdık ve ne zaman hangisinin daha uygun olduğunu konuştuk. Şahsi kanaatim, trigger'ı 'sessiz garantici' olarak küçük tutmak, iş mantığını ise tamamen uygulamada bırakmak en sürdürülebilir denge. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.