Veritabanı yönetiminde işlerin gerçekten karmaşıklaştığı noktalardan biri, uygulama katmanında mı yoksa veritabanı katmanında mı iş mantığını tutacağın kararıdır. Stored procedure’ler ve trigger’lar bu tartışmanın tam merkezinde yer alır. Yıllarca “her şeyi uygulama katmanında yap” diyenler oldu, ama gerçek dünyada bu her zaman mümkün değil. Özellikle birden fazla uygulamanın aynı veritabanını kullandığı ortamlarda, ya da performansın kritik olduğu sistemlerde stored procedure ve trigger’lar hayat kurtarır. Bu yazıda MySQL üzerinde bu iki güçlü özelliği, gerçek senaryolarla ve pratik örneklerle inceleyeceğiz.
Stored Procedure Nedir ve Neden Kullanılır?
Stored procedure, veritabanı sunucusunda saklanan ve tekrar tekrar çağrılabilen SQL kod bloklarıdır. Bir nevi veritabanı fonksiyonu gibi düşünebilirsin. Avantajları şu şekilde sıralanabilir:
- Ağ trafiğini azaltır: Onlarca sorgu yerine tek bir prosedür çağrısı gider
- Kod tekrarını önler: Aynı mantığı her uygulamada yazmak zorunda kalmazsın
- Güvenliği artırır: Tablolara direkt erişim yerine prosedür üzerinden erişim sağlanabilir
- Performans: MySQL ilk çalıştırmada execution plan oluşturur, sonraki çağrılarda bu plan kullanılır
- Merkezi yönetim: İş mantığı değiştiğinde tek bir yeri güncellersin
Tabii dezavantajları da var. Debug etmek zordur, versiyon kontrolü uygulamak karmaşıklaşabilir ve taşınabilirlik (portability) sorunu çıkabilir. Ama bunları bilerek kullandığında çok güçlü bir araç.
İlk Stored Procedure’ü Yazmak
MySQL’de stored procedure yazmadan önce delimiter’ı değiştirmemiz gerekir. Çünkü procedure içindeki noktalı virgüller MySQL’i karıştırır.
mysql -u root -p
mysql> DELIMITER //
CREATE PROCEDURE kullanici_ozeti(IN kullanici_id INT)
BEGIN
DECLARE toplam_siparis INT DEFAULT 0;
DECLARE toplam_tutar DECIMAL(10,2) DEFAULT 0.00;
SELECT COUNT(*), SUM(tutar)
INTO toplam_siparis, toplam_tutar
FROM siparisler
WHERE musteri_id = kullanici_id;
SELECT
k.ad,
k.soyad,
k.email,
toplam_siparis AS siparis_sayisi,
toplam_tutar AS toplam_harcama
FROM kullanicilar k
WHERE k.id = kullanici_id;
END //
DELIMITER ;
Bu prosedürü çağırmak için:
mysql> CALL kullanici_ozeti(42);
Basit ama işlevsel. Şimdi daha gerçekçi bir senaryoya geçelim.
Gerçek Dünya Senaryosu: Stok Yönetimi
Bir e-ticaret sisteminde sipariş oluşturulduğunda stoktan düşülmesi gerekir. Bu işlemi birden fazla adımdan oluşan bir transaction içinde yapmak en doğrusu. Hem siparişi kaydet, hem stoku güncelle, hem de log tut.
DELIMITER //
CREATE PROCEDURE siparis_olustur(
IN p_musteri_id INT,
IN p_urun_id INT,
IN p_miktar INT,
OUT p_siparis_id INT,
OUT p_hata_mesaji VARCHAR(255)
)
BEGIN
DECLARE v_stok INT DEFAULT 0;
DECLARE v_fiyat DECIMAL(10,2) DEFAULT 0.00;
DECLARE v_toplam DECIMAL(10,2) DEFAULT 0.00;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET p_siparis_id = -1;
SET p_hata_mesaji = 'İşlem sırasında hata oluştu, geri alındı.';
END;
-- Stok kontrolü
SELECT stok_miktari, satis_fiyati
INTO v_stok, v_fiyat
FROM urunler
WHERE id = p_urun_id
FOR UPDATE;
IF v_stok < p_miktar THEN
SET p_siparis_id = -1;
SET p_hata_mesaji = 'Yeterli stok yok.';
ELSE
START TRANSACTION;
SET v_toplam = v_fiyat * p_miktar;
-- Sipariş kaydı oluştur
INSERT INTO siparisler (musteri_id, urun_id, miktar, tutar, tarih)
VALUES (p_musteri_id, p_urun_id, p_miktar, v_toplam, NOW());
SET p_siparis_id = LAST_INSERT_ID();
-- Stok güncelle
UPDATE urunler
SET stok_miktari = stok_miktari - p_miktar
WHERE id = p_urun_id;
-- Hareket logu
INSERT INTO stok_hareketleri (urun_id, islem_tipi, miktar, referans_id, tarih)
VALUES (p_urun_id, 'SATIS', p_miktar, p_siparis_id, NOW());
COMMIT;
SET p_hata_mesaji = '';
END IF;
END //
DELIMITER ;
Bunu uygulamadan şöyle çağırırsın:
mysql> CALL siparis_olustur(5, 101, 3, @siparis_id, @hata);
mysql> SELECT @siparis_id, @hata;
OUT parametreleri sayesinde işlemin sonucunu uygulama katmanına temiz bir şekilde iletebiliyorsun.
Döngüler ve Koşullar: Prosedürlerde Akış Kontrolü
Stored procedure’lerin güçlü olduğu noktalardan biri de programatik yapıları desteklemesi. IF/ELSE, WHILE, REPEAT ve LOOP yapılarını kullanabilirsin.
DELIMITER //
CREATE PROCEDURE aylik_rapor_olustur(IN p_yil INT, IN p_ay INT)
BEGIN
DECLARE v_baslangic DATE;
DECLARE v_bitis DATE;
DECLARE v_gun INT DEFAULT 1;
DECLARE v_toplam_gun INT;
SET v_baslangic = DATE(CONCAT(p_yil, '-', LPAD(p_ay, 2, '0'), '-01'));
SET v_bitis = LAST_DAY(v_baslangic);
SET v_toplam_gun = DAY(v_bitis);
-- Önceki rapor varsa sil
DELETE FROM gunluk_raporlar
WHERE rapor_yil = p_yil AND rapor_ay = p_ay;
-- Her gün için rapor satırı oluştur
WHILE v_gun <= v_toplam_gun DO
INSERT INTO gunluk_raporlar (rapor_yil, rapor_ay, gun,
siparis_sayisi, toplam_ciro)
SELECT
p_yil,
p_ay,
v_gun,
COUNT(*),
COALESCE(SUM(tutar), 0)
FROM siparisler
WHERE DATE(tarih) = DATE_ADD(v_baslangic, INTERVAL (v_gun - 1) DAY);
SET v_gun = v_gun + 1;
END WHILE;
SELECT CONCAT(p_yil, '/', p_ay, ' raporu oluşturuldu. ',
v_toplam_gun, ' gün işlendi.') AS sonuc;
END //
DELIMITER ;
Trigger’a Giriş: Otomatik Tepki Mekanizması
Trigger’lar, bir tabloda INSERT, UPDATE veya DELETE işlemi gerçekleştiğinde otomatik olarak çalışan kod bloklarıdır. BEFORE veya AFTER olarak tanımlanabilirler.
- BEFORE INSERT: Kayıt eklenmeden önce çalışır, veriyi değiştirebilirsin
- AFTER INSERT: Kayıt eklendikten sonra çalışır, başka tablolara işlem yapabilirsin
- BEFORE UPDATE: Güncelleme öncesi, eski ve yeni değerlere erişebilirsin
- AFTER UPDATE: Güncelleme sonrası log tutmak için idealdir
- BEFORE DELETE: Silme öncesi kontrol yapabilirsin
- AFTER DELETE: Silme sonrası temizlik işlemleri için kullanılır
Trigger içinde OLD ve NEW anahtar kelimelerini kullanırsın. OLD silinen veya güncellenen önceki değeri, NEW eklenen veya güncellenen yeni değeri temsil eder.
İlk Trigger: Audit Log Tutmak
En klasik trigger kullanım senaryosu audit logging’dir. Kritik tablolarda kim ne zaman ne değiştirdi bilgisini tutmak.
DELIMITER //
CREATE TRIGGER kullanici_guncelleme_log
AFTER UPDATE ON kullanicilar
FOR EACH ROW
BEGIN
IF OLD.email != NEW.email THEN
INSERT INTO audit_log (tablo_adi, kayit_id, alan_adi,
eski_deger, yeni_deger, degisiklik_tarihi,
kullanici_ip)
VALUES ('kullanicilar', NEW.id, 'email',
OLD.email, NEW.email, NOW(),
@kullanici_ip);
END IF;
IF OLD.telefon != NEW.telefon OR
(OLD.telefon IS NULL AND NEW.telefon IS NOT NULL) THEN
INSERT INTO audit_log (tablo_adi, kayit_id, alan_adi,
eski_deger, yeni_deger, degisiklik_tarihi,
kullanici_ip)
VALUES ('kullanicilar', NEW.id, 'telefon',
OLD.telefon, NEW.telefon, NOW(),
@kullanici_ip);
END IF;
END //
DELIMITER ;
Session değişkeni olan @kullanici_ip‘yi uygulama katmanından bağlantı başında set edebilirsin:
mysql> SET @kullanici_ip = '192.168.1.100';
Gerçek Dünya Senaryosu: Stok Uyarı Sistemi
Stok seviyesi belirli bir eşiğin altına düştüğünde otomatik uyarı kaydı oluşturalım.
DELIMITER //
CREATE TRIGGER stok_uyari_kontrolu
AFTER UPDATE ON urunler
FOR EACH ROW
BEGIN
DECLARE v_esik INT DEFAULT 10;
-- Stok azaldı mı kontrol et
IF NEW.stok_miktari < OLD.stok_miktari THEN
-- Kritik stok seviyesine düştü mü?
IF NEW.stok_miktari <= v_esik AND OLD.stok_miktari > v_esik THEN
INSERT INTO stok_uyarilari (
urun_id,
urun_adi,
mevcut_stok,
esik_deger,
uyari_tipi,
olusturulma_tarihi,
okundu
)
SELECT
NEW.id,
NEW.urun_adi,
NEW.stok_miktari,
v_esik,
CASE
WHEN NEW.stok_miktari = 0 THEN 'KRITIK'
WHEN NEW.stok_miktari <= 5 THEN 'DUSUK'
ELSE 'UYARI'
END,
NOW(),
0;
END IF;
-- Tamamen bitti
IF NEW.stok_miktari = 0 AND OLD.stok_miktari > 0 THEN
UPDATE urunler
SET aktif = 0
WHERE id = NEW.id;
END IF;
END IF;
END //
DELIMITER ;
Bu trigger sayesinde stok yönetim panelinde “okunmamış uyarılar” özelliği kolayca hayata geçirilebilir.
Prosedür Yönetimi ve Bakımı
Sistemde hangi prosedürlerin olduğunu görmek:
-- Tüm stored procedure'leri listele
SHOW PROCEDURE STATUS WHERE Db = 'veritabanim';
-- Belirli bir prosedürün kodunu gör
SHOW CREATE PROCEDURE siparis_olustur;
-- Information schema'dan sorgula
SELECT ROUTINE_NAME, ROUTINE_TYPE, CREATED, LAST_ALTERED
FROM information_schema.ROUTINES
WHERE ROUTINE_SCHEMA = 'veritabanim'
AND ROUTINE_TYPE = 'PROCEDURE'
ORDER BY LAST_ALTERED DESC;
Trigger’ları listelemek için:
-- Tüm trigger'ları listele
SHOW TRIGGERS FROM veritabanim;
-- Belirli tablonun trigger'larını gör
SHOW TRIGGERS FROM veritabanim LIKE 'kullanicilar';
-- Information schema'dan detaylı bilgi
SELECT TRIGGER_NAME, EVENT_MANIPULATION, EVENT_OBJECT_TABLE,
ACTION_TIMING, CREATED
FROM information_schema.TRIGGERS
WHERE TRIGGER_SCHEMA = 'veritabanim'
ORDER BY EVENT_OBJECT_TABLE, ACTION_TIMING;
Performans Konuları
Stored procedure ve trigger kullanırken dikkat etmen gereken bazı performans tuzakları var.
Trigger’lar her satır için çalışır. FOR EACH ROW bu demek. Eğer 100.000 satır UPDATE ediyorsan, trigger 100.000 kez tetiklenir. Bu ciddi performans sorunlarına yol açabilir.
-- Trigger'ın tetiklenme sayısını izlemek için
SHOW STATUS LIKE 'Handler_update';
-- Slow query log'u aktifleştir
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
Prosedürlerde cursor kullanımından mümkün olduğunca kaçın. Set bazlı operasyonlar her zaman daha hızlıdır. Cursor kullanmak zorundaysan şöyle tanımlarsın:
DELIMITER //
CREATE PROCEDURE toplu_indirim_uygula(IN p_kategori_id INT, IN p_oran DECIMAL(5,2))
BEGIN
DECLARE v_bitti INT DEFAULT 0;
DECLARE v_urun_id INT;
DECLARE v_fiyat DECIMAL(10,2);
DECLARE urun_cursor CURSOR FOR
SELECT id, satis_fiyati FROM urunler
WHERE kategori_id = p_kategori_id AND aktif = 1;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET v_bitti = 1;
OPEN urun_cursor;
urun_dongusu: LOOP
FETCH urun_cursor INTO v_urun_id, v_fiyat;
IF v_bitti = 1 THEN
LEAVE urun_dongusu;
END IF;
UPDATE urunler
SET satis_fiyati = v_fiyat * (1 - p_oran / 100),
guncelleme_tarihi = NOW()
WHERE id = v_urun_id;
END LOOP urun_dongusu;
CLOSE urun_cursor;
-- Aslında bunu tek sorguda yapmak çok daha iyi
-- UPDATE urunler SET satis_fiyati = satis_fiyati * (1 - p_oran/100)
-- WHERE kategori_id = p_kategori_id AND aktif = 1;
END //
DELIMITER ;
Yukarıdaki örnekte cursor kullanımını gösterdim ama yorum satırında da belirttiğim gibi bu işi tek bir UPDATE ile yapmak çok daha verimli. Cursor’un mantıklı olduğu durumlar gerçekten çok sınırlı.
Güvenlik ve Yetkilendirme
Stored procedure’ler güvenlik açısından çok değerlidir. Kullanıcılara tabloya direkt erişim yerine sadece prosedür çalıştırma yetkisi verebilirsin.
-- Kullanıcıya sadece belirli prosedürleri çalıştırma yetkisi ver
GRANT EXECUTE ON PROCEDURE veritabanim.siparis_olustur TO 'uygulama_kullanici'@'192.168.1.%';
GRANT EXECUTE ON PROCEDURE veritabanim.kullanici_ozeti TO 'uygulama_kullanici'@'192.168.1.%';
-- Tabloya direkt erişim verme
-- REVOKE ALL ON veritabanim.* FROM 'uygulama_kullanici'@'192.168.1.%';
-- Yetkileri kontrol et
SHOW GRANTS FOR 'uygulama_kullanici'@'192.168.1.%';
DEFINER ve INVOKER kavramlarını da bilmek gerekir:
- DEFINER: Prosedür, onu oluşturan kullanıcının yetkileriyle çalışır (varsayılan)
- INVOKER: Prosedür, onu çağıran kullanıcının yetkileriyle çalışır
-- DEFINER belirterek prosedür oluştur
CREATE DEFINER='admin'@'localhost' PROCEDURE guvenli_rapor()
SQL SECURITY DEFINER
BEGIN
-- Bu prosedürü kim çağırırsa çağırsın, admin yetkisiyle çalışır
SELECT * FROM gizli_tablo;
END;
Trigger ve Prosedür Silme ve Güncelleme
MySQL’de stored procedure ve trigger’ları güncellemek için önce silip sonra yeniden oluşturmak gerekir. ALTER PROCEDURE sadece bazı özellikleri değiştirir, kodu değiştiremezsiniz.
-- Prosedür sil
DROP PROCEDURE IF EXISTS siparis_olustur;
-- Trigger sil
DROP TRIGGER IF EXISTS stok_uyari_kontrolu;
-- Toplu temizlik için (dikkatli kullan!)
-- Veritabanındaki tüm trigger'ları bul ve DROP script'i oluştur
SELECT CONCAT('DROP TRIGGER IF EXISTS ', TRIGGER_NAME, ';')
FROM information_schema.TRIGGERS
WHERE TRIGGER_SCHEMA = 'veritabanim';
Production ortamında prosedür güncellemesi yaparken şöyle bir yaklaşım izle:
-- Önce yedek al
mysqldump --routines --no-data -u root -p veritabanim > routines_backup.sql
-- Sonra güncelle
mysql -u root -p veritabanim < yeni_prosedur.sql
-- Doğrula
mysql -u root -p -e "SHOW CREATE PROCEDURE veritabanim.siparis_olusturG"
Sık Yapılan Hatalar
Yıllar içinde gördüğüm en yaygın hatalar şunlar:
- Trigger içinde aynı tabloya yazma: Trigger tetiklendiği tablo üzerinde işlem yaparsan sonsuz döngüye girersin. MySQL bunu
SQLEXCEPTIONile durdurur ama yine de tehlikeli. - Transaction kontrolünü unutmak: Prosedür içinde hata olduğunda ROLLBACK yapmayı unutmak veri tutarsızlığına yol açar. EXIT HANDLER kullanımı şart.
- NULL kontrolü yapmamak:
IF OLD.alan != NEW.alanifadesi NULL değerlerde doğru çalışmaz.COALESCEveyaIS NULLkullan. - Test ortamında çalışıp production’da çalışmamak: MySQL versiyonları arasında sözdizimi farklılıkları olabilir.
SELECT VERSION();ile kontrol et. - Çok fazla iş mantığı koymak: Prosedürleri her şeyi yapan dev bloklara dönüştürmek. Tek sorumluluk ilkesi veritabanında da geçerli.
Sonuç
Stored procedure ve trigger’lar, doğru kullanıldığında veritabanı yönetimini çok daha güçlü ve güvenli hale getirir. Özellikle çok katmanlı uygulamalarda ya da birden fazla servisin aynı veritabanını kullandığı mimarilerde iş mantığını veritabanı katmanında tutmak büyük avantaj sağlar.
Benim kişisel önerim şu: Trigger’ları audit log ve otomatik hesaplama gibi basit, net amaçlar için kullan. Karmaşık iş mantığını trigger içine gömmek debug cehennemine dönüşür. Stored procedure’leri ise transaction gerektiren çok adımlı işlemler ve güvenlik katmanı olarak düşün.
Şunu da unutma: Bu araçların en büyük düşmanı dokümantasyon eksikliği. Her prosedürü ve trigger’ı oluşturduğunda amacını, parametrelerini ve bağımlılıklarını bir yere not et. Altı ay sonra o kodu okuyacak kişi muhtemelen sen olacaksın ve o an kendine çok teşekkür edeceksin.