Ders 13 / 20
Tetikleyiciler
Tetikleyici tanımı, BEFORE ve AFTER ile satır başına çalışma, denetim kaydı ve türetilmiş durum güncelleme, RAISE ile kural dayatma, görünüm üzerinde INSTEAD OF ve özyineleme riski.
İçindekiler
Önceki derste sunucu tarafındaki kod hep çağrıldı: yordam CALL ile, işlev sorgu içinde
adıyla. Çağrıldığı yerden okunduğu için ne yaptığı görünürdü.
Tetikleyici (trigger) çağrılmaz. Bir tabloya ekleme, güncelleme ya da silme yapıldığında
motor onu kendiliğinden çalıştırır. Bu, güçlü bir araç ve aynı ölçüde bir tuzaktır: sıradan
görünen bir UPDATE deyimi, metninde hiç geçmeyen tablolara yazabilir.
Tanım ve Çalışma Anı
Tetikleyici üç şeyle tanımlanır: hangi tabloyu izlediği, hangi olayda çalıştığı ve olayın öncesinde mi sonrasında mı çalıştığı.
BEFORE: deyim satırı işlemeden önce çalışır. Değer düzeltmek ya da işlemi reddetmek için kullanılır.AFTER: satır işlendikten sonra çalışır. Başka tablolara yazmak için kullanılır.INSTEAD OF: satır işlenmez, onun yerine tetikleyici çalışır. Görünümler içindir.
FOR EACH ROW yazımı, tetikleyicinin etkilenen her satır için ayrı çalıştığını söyler.
Gövde içinde iki özel ad kullanılabilir: NEW, satırın yeni hâlini; OLD, eski hâlini
verir. Ekleme olayında OLD, silme olayında NEW tanımsızdır.
İlk kullanım, iki işi birden yapan bir çift tetikleyicidir: türetilmiş durumu güncellemek ve denetim kaydı yazmak.
sqlite3 -box -header <<'SQL' CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, rafta INT NOT NULL DEFAULT 1); CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT); CREATE TABLE denetim(id INTEGER PRIMARY KEY, olay TEXT, odunc_id INT, ayrinti TEXT); INSERT INTO kitap VALUES (1,'Kayip Zaman',1),(2,'Sessiz Ev',1); CREATE TRIGGER odunc_acildi AFTER INSERT ON odunc FOR EACH ROW BEGIN UPDATE kitap SET rafta = 0 WHERE id = NEW.kitap_id; INSERT INTO denetim(olay, odunc_id, ayrinti) VALUES ('acildi', NEW.id, 'kitap ' || NEW.kitap_id || ' uye ' || NEW.uye_id); END; CREATE TRIGGER odunc_kapandi AFTER UPDATE OF iade ON odunc FOR EACH ROW WHEN OLD.iade IS NULL AND NEW.iade IS NOT NULL BEGIN UPDATE kitap SET rafta = 1 WHERE id = NEW.kitap_id; INSERT INTO denetim(olay, odunc_id, ayrinti) VALUES ('kapandi', NEW.id, 'iade ' || NEW.iade); END; INSERT INTO odunc(id, kitap_id, uye_id, alis) VALUES (1, 1, 4, '2024-03-11'); UPDATE odunc SET iade = '2024-03-20' WHERE id = 1; SELECT id, baslik, rafta FROM kitap ORDER BY id; SELECT id, olay, odunc_id, ayrinti FROM denetim ORDER BY id; SQL
┌────┬─────────────┬───────┐ │ id │ baslik │ rafta │ ├────┼─────────────┼───────┤ │ 1 │ Kayip Zaman │ 1 │ │ 2 │ Sessiz Ev │ 1 │ └────┴─────────────┴───────┘ ┌────┬─────────┬──────────┬─────────────────┐ │ id │ olay │ odunc_id │ ayrinti │ ├────┼─────────┼──────────┼─────────────────┤ │ 1 │ acildi │ 1 │ kitap 1 uye 4 │ │ 2 │ kapandi │ 1 │ iade 2024-03-20 │ └────┴─────────┴──────────┴─────────────────┘
İki deyim yazıldı: bir INSERT ve bir UPDATE. Dördü de olan şu: ödünç kaydı açıldı,
kitap raftan düştü, denetim satırı yazıldı; sonra kayıt kapandı, kitap rafa döndü, ikinci
denetim satırı yazıldı. Sonuçtaki rafta değerinin 1 olması, ikinci tetikleyicinin
birincinin etkisini geri aldığını gösterir.
WHEN yan tümcesi, tetikleyicinin yalnız belirli satırlarda çalışmasını sağlar. Buradaki
koşul, kaydın gerçekten kapandığı güncellemeleri ayırıyor; iade tarihinin düzeltildiği
bir güncelleme kitabı ikinci kez rafa koymayacak.
AFTER UPDATE OF iade yazımı tetikleyiciyi tek sütuna bağlar. Sütun listesi verilmezse
tetikleyici, tablodaki herhangi bir sütunun güncellenmesinde çalışır — genellikle
istenmeyen bir genişliktir.
Kural Dayatmak
BEFORE tetikleyicileri işlemi reddedebilir. Standart SQL bunun için sinyal deyimi
tanımlar; motorlarda karşılığı bir hata yükseltme işlevidir. Aşağıdaki tetikleyici, rafta
olmayan bir kitabın ödünç verilmesini engeller:
sqlite3 -box -header <<'SQL' CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, rafta INT NOT NULL DEFAULT 1); CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT); INSERT INTO kitap VALUES (1,'Kayip Zaman',0),(2,'Sessiz Ev',1); CREATE TRIGGER odunc_kontrol BEFORE INSERT ON odunc FOR EACH ROW WHEN (SELECT rafta FROM kitap WHERE id = NEW.kitap_id) = 0 BEGIN SELECT RAISE(ABORT, 'kitap rafta degil'); END; INSERT INTO odunc(id, kitap_id, uye_id, alis) VALUES (1, 2, 4, '2024-03-11'); INSERT INTO odunc(id, kitap_id, uye_id, alis) VALUES (2, 1, 7, '2024-03-11'); SELECT id, kitap_id, uye_id FROM odunc ORDER BY id; SQL
Runtime error near line 13: kitap rafta degil (19) ┌────┬──────────┬────────┐ │ id │ kitap_id │ uye_id │ ├────┼──────────┼────────┤ │ 1 │ 2 │ 4 │ └────┴──────────┴────────┘
Rafta olan kitabın ödüncü geçti, olmayanınki tetikleyicinin verdiği iletiyle reddedildi. İleti metni tetikleyicide yazılmıştır; uygulamaya ulaşan hata, kısıt ihlali hatalarıyla aynı yoldan gelir.
Bu araç kısıtların yerini tutmaz. Bir kural bir sütunun tek başına değeriyle ifade
edilebiliyorsa CHECK kısıtı, başka bir tablodaki satırın varlığıyla ifade edilebiliyorsa
yabancı anahtar kullanılmalıdır; ikisi de bildirimseldir, planlayıcı tarafından bilinir ve
tetikleyiciden ucuzdur. Tetikleyici, ancak birden çok tabloyu birlikte inceleyen kurallar
için gerekir.
Görünüm Üzerinde
Karmaşık bir görünüm doğrudan güncellenemez: motor, görünüm satırındaki bir değişikliğin
temel tablolara nasıl yansıyacağını türetemez. INSTEAD OF tetikleyicisi bu eşlemeyi elle
tanımlar:
sqlite3 -box -header <<'SQL' CREATE TABLE odunc(id INTEGER PRIMARY KEY, kitap_id INT, uye_id INT, alis TEXT, iade TEXT); INSERT INTO odunc VALUES (1,1,4,'2024-03-01',NULL),(2,3,7,'2024-03-04',NULL); CREATE VIEW acik_odunc AS SELECT id, kitap_id, uye_id, alis FROM odunc WHERE iade IS NULL; CREATE TRIGGER acik_odunc_kapat INSTEAD OF DELETE ON acik_odunc FOR EACH ROW BEGIN UPDATE odunc SET iade = '2024-03-31' WHERE id = OLD.id; END; DELETE FROM acik_odunc WHERE id = 1; SELECT id, alis, COALESCE(iade,'(acik)') AS iade FROM odunc ORDER BY id; SQL
┌────┬────────────┬────────────┐ │ id │ alis │ iade │ ├────┼────────────┼────────────┤ │ 1 │ 2024-03-01 │ 2024-03-31 │ │ 2 │ 2024-03-04 │ (acik) │ └────┴────────────┴────────────┘
Yazılan deyim bir silmeydi; olan bir güncellemedir. Satır tabloda duruyor, yalnız iade tarihi doldu. Görünümün açısından bakınca doğru — satır artık “açık ödünç” değil — ama deyimin metni bunu söylemiyor. Örtük yan etkinin en açık örneği budur.
Özyineleme
Bir tetikleyici, kendi izlediği tabloyu güncellerse yeniden çalışabilir. Motorlar bunu ya baştan engeller ya da bir derinlik sınırıyla durdurur:
sqlite3 -box -header <<'SQL' PRAGMA recursive_triggers = ON; CREATE TABLE kitap(id INTEGER PRIMARY KEY, baslik TEXT, sayac INT NOT NULL DEFAULT 0); INSERT INTO kitap VALUES (1,'Kayip Zaman',0); CREATE TRIGGER sayac_artir AFTER UPDATE OF sayac ON kitap FOR EACH ROW BEGIN UPDATE kitap SET sayac = NEW.sayac + 1 WHERE id = NEW.id; END; UPDATE kitap SET sayac = 1 WHERE id = 1; SELECT id, sayac FROM kitap; SQL
Runtime error near line 11: too many levels of trigger recursion ┌────┬───────┐ │ id │ sayac │ ├────┼───────┤ │ 1 │ 0 │ └────┴───────┘
Tek bir UPDATE yazıldı; tetikleyici kendini çağırdı ve zincir derinlik sınırında kesildi.
Deyim tümüyle geri alındığı için sayaç sıfırda kaldı — atomiklik burada da geçerlidir.
Özyinelemenin açık ya da kapalı olması motora göre değişir; kapalıyken aynı tanım sessizce tek tur çalışır ve sorun görünmez. İki motor arasında taşınan bir şemanın davranışının değişebileceği yerlerden biri budur.
Örtük Yan Etkinin Bedeli
Tetikleyicilerin sağladığı yarar açıktır: kural, onu tetikleyen koddan bağımsız olarak her zaman uygulanır. Uygulamanın hangi katmanından gelirse gelsin, ödünç kaydı açıldığında denetim satırı yazılır. Bu, kuralın atlanamamasını güvenceye alır.
Bedeli, deyimin metni ile etkisinin ayrışmasıdır. Bunun somut sonuçları şunlardır:
- Hata ayıklama. Beklenmedik bir satır değişikliğinin kaynağı sorgu kütüklerinde görünmez; şemadaki tetikleyicilerin okunması gerekir.
- Sıra belirsizliği. Aynı olaya bağlı birden çok tetikleyicinin çalışma sırası standartta belirlenmemiştir; birbirine bağımlı iki tetikleyici yazmak kırılgandır.
- Toplu işlemler. Satır başına çalışan tetikleyici, yüz binlik bir güncellemede yüz bin kez çalışır; toplu yükleme yollarının bazılarında ise hiç çalışmayabilir.
- Görünmez maliyet. Basit görünen bir deyimin planı, tetikleyici gövdesindeki sorguları da içerir.
Ölçü şudur: tetikleyici, veri bütünlüğünü koruyan ve atlanmaması gereken kurallar için uygundur — denetim kaydı, türetilmiş sütunun tutarlılığı, çok tablolu kısıtlar. İş akışı kararları — bildirim göndermek, ücret hesaplamak, dış sistem çağırmak — uygulama katmanında kalmalıdır. Yazıldıklarında da kısa tutulmalı ve şemayla birlikte belgelenmelidir.
Özet
- Tetikleyici çağrılmaz; izlediği tabloda olay gerçekleştiğinde motor tarafından çalıştırılır
ve
NEWileOLDüzerinden satırın iki hâline erişir. BEFOREdüzeltme ve reddetme,AFTERbaşka tablolara yazma,INSTEAD OFgörünüm güncellemesi içindir;WHENyan tümcesi çalışmayı daraltır.- Tek sütunla ifade edilebilen kurallar
CHECKya da yabancı anahtarla bildirilmelidir; tetikleyici çok tablolu kurallar için gerekir. - Kendi tablosunu güncelleyen tetikleyici özyineleme üretir; motor bunu derinlik sınırıyla keser ve deyimin tamamı geri alınır.
- Örtük yan etki, deyimin metni ile etkisini ayırır; hata ayıklamayı, sıra varsayımlarını ve toplu işlem maliyetini etkiler.
Sonraki Adım
Bu konu boyunca sorgular doğru sonucu vermeye odaklandı: alt sorgu doğru kümeyi seçti,
pencere doğru çerçeveyi gördü, işlem doğru anda kesinleşti. Doğruluk tek ölçüt değildir.
İlişkili alt sorgular dersinde iki yazımın aynı sonucu 200’e karşı 25 satır ziyaretiyle
ürettiği ölçülmüş, farkı yaratanın erişim yolu olduğu görülmüştü; o derste bir dizin
eklendiğinde plan SCAN yerine SEARCH demişti. Sonraki konu bu noktadan başlar. İlk ders,
dizinin ne olduğunu ve aramanın maliyetini nasıl düşürdüğünü kurar; oradan sorgu planını
okumaya ve aynı sonucu daha az işle veren yeniden yazımlara geçilecektir.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.