İçeriğe geç
academia.sh

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 NEW ile OLD üzerinden satırın iki hâline erişir.
  • BEFORE düzeltme ve reddetme, AFTER başka tablolara yazma, INSTEAD OF görünüm güncellemesi içindir; WHEN yan tümcesi çalışmayı daraltır.
  • Tek sütunla ifade edilebilen kurallar CHECK ya 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.

Aramak için yazmaya başlayın.

↑↓ Esc gezin · aç · kapat