Wyzwalacze bazy danych są to procedury wywoływane (odpalane) przez system przy zajściu odpowiedniego zdarzenia dotyczącego tabel w bazie danych. Będą więc miały budowę podobną do procedur składowanych, lecz nie będziemy mogli ich uruchomić samodzielnie, poleceniem EXECUTE
, lecz będą uruchamiane automatycznie.
Wyzwalacze bazy danych służą do:
Wyzwalacze zostały wprowadzone do Standardu SQL:1999, aczkolwiek sposób ich definiowania i wykorzystania w Transact-SQL i PL/SQL znacznie się różni.
Wyzwalacz będzie zawsze związany z określoną tabelą oraz uruchamiany przez jedną lub więcej instrukcji DML (INSERT
, UPDATE
, DELETE
).
Analogicznie, jak w przypadku procedur składowanych, możemy utworzyć wyzwalacz instrukcją CREATE
, a zmodyfikować istniejący poleceniem ALTER
, znajdując się w oknie kodu. Interfejs Management Studio wyświetla wyzwalacze po rozwinięciu wybranej tabeli oraz gałęzi Triggers. Z tego miejsca możemy je też tworzyć i modyfikować.
Składnia:
CREATE TRIGGER nazwa_wyzwalacza ON tabela FOR instrukcje_DML AS instrukcje Transact-SQL
Instrukcje DML, które mogą wywołać wyzwalacz to:
INSERT
UPDATE
DELETE
Po słowie FOR
możemy napisać jedną, dwie lub wszystkie trzy instrukcje (oddzielone przecinkami).
Wyzwalacz odpalany jest zawsze PO wykonaniu wywołującej go instrukcji DML. Jest jednak odpalany w obrębie tej samej transakcji, co wywołująca go instrukcja. Dzięki temu możemy wycofać efekty tej instrukcji poleceniem ROLLBACK
.
Odwołania do starych i nowych wartości modyfikowanych danych są możliwe dzięki wirtualnym tabelom o nazwach INSERTED
i DELETED
, zawierających odpowiednio nowe i stare wartości. Aby się do nich odwołać, musimy użyć instrukcji SELECT
tak, jakby były to normalne tabele bazy danych. W przypadku, gdy wyzwalacz został uruchomiony przez instrukcję INSERT
, tylko tabela INSERTED
będzie zawierała rekordy. Tabela DELETED
będzie pusta. Gdy odpalającą wyzwalacz instrukcją jest DELETE
, tylko DELETED
zawiera rekordy. W przypadku UPDATE
obie tabele zawierają rekordy: INSERTED
zawiera nowe wartości (te, na które zmieniamy), a DELETED
stare (nadpisane).
Tabele te są tylko do odczytu, czyli modyfikacja danych bezpośrednio w nich nie jest możliwa. Jeśli więc chcemy wewnątrz wyzwalacza wprowadzić modyfikację danych, musimy wykonywać instrukcje DML na rzeczywistych tabelach. Pamiętajmy, że dane w tych tabelach są danymi już zaktualizowanymi przez instrukcję uruchamiającą wyzwalacz.
Istniejące wyzwalacze mogą być włączane i wyłączane instrukcją:
ENABLE / DISABLE TRIGGER nazwa_wyzwalacza ON nazwa_tabeli
W poniższym przykładzie wyzwalacz ma za zadanie nie pozwolić usunąć wiersza z tabeli. Będzie uruchamiany automatycznie po każdym wywołaniu instrukcji DELETE
na tabeli „emp”. Ponieważ jednak wyzwalacz jest uruchamiany po wykonaniu operacji DELETE
, musimy przy pomocy instrukcji ROLLBACK
wycofać wprowadzone zmiany. Pamiętajmy, że wyzwalacz jest uruchomiony w tej samej transakcji co instrukcja.
CREATE TRIGGER wyzw ON Emp FOR DELETE AS ROLLBACK
W kolejnym przykładzie chcielibyśmy, aby nowo wstawiani pracownicy nie mogli mieć zarobków równych zero. Gdy taki pracownik zostanie wprowadzony, wyzwalacz wycofa zmiany i zgłosi błąd (oczywiście można to osiągnąć używając więzów spójności typu CHECK
).
CREATE TRIGGER wyzw ON Emp FOR INSERT AS DECLARE @sal DECIMAL(6,2) SELECT @sal = sal FROM inserted IF @sal = 0 BEGIN ROLLBACK RAISERROR('Nie można wstawić pracownika z pensją równą zero!',1,2) END
Powyższy przykład nie zawsze zadziała poprawnie. W Transact-SQL wyzwalacz jest uruchamiany dla całej instrukcji DML. Instrukcja DML może operować na 1 wierszu (taka sytuacja jest prosta) lub na wielu wierszach. W przypadku wielu wierszy tabele INSERTED
i/lub DELETED
będą również zawierać wiele wierszy. Aby taką sytuację obsłużyć, będziemy musieli zastanowić się nad kodem, który zadziała poprawnie dla dowolnej liczby wierszy. Czasem będziemy musieli przejrzeć wszystkie rekordy tabel INSERTED
i/lub DELETED
przy pomocy kursora.
Powyższy przykład będzie więc działał poprawnie tylko gdy instrukcja INSERT
która go wywołała, operowała na jednym wierszu.
Rozwiązanie bardziej uniwersalne:
CREATE TRIGGER pensja ON emp FOR INSERT AS BEGIN IF EXISTS (SELECT 'x' FROM inserted WHERE sal = 0) BEGIN ROLLBACK RAISERROR ('Nie można wstawić pracownika z pensją równą 0', 1, 2) END
W tym przypadku, jeśli którykolwiek wiersz znajdujący się w wirtualnej tabeli INSERTED
będzie miał nieprawidłową pensję, transakcja zostanie wycofana.
W kolejnym przykładzie chcielibyśmy, aby wstawiani lub modyfikowani pracownicy mieli pensję większą niż 1000. Do rozwiązania tego zadania wystarczyłyby deklaratywne więzy typu CHECK
. Więzy spójności wycofają jednak całą instrukcję. My chcielibyśmy, aby poprawne rekordy zostały wstawione lub zmodyfikowane, a odrzucone tylko te, których pensja jest za mała.
CREATE TRIGGER zarobki ON emp FOR INSERT, UPDATE AS DECLARE @ile INT, @old_sal INT, @new_sal INT, @empno INT SELECT @ile = COUNT(*) from deleted IF @ile = 0 BEGIN -- jeśli 0 rekordów w deleted, to był to INSERT DECLARE kurs CURSOR FOR SELECT empno, sal FROM inserted OPEN kurs FETCH NEXT FROM kurs INTO @empno, @new_sal WHILE @@FETCH_STATUS = 0 BEGIN IF @new_sal < 1000 DELETE FROM emp WHERE empno = @empno FETCH NEXT FROM kurs INTO @empno, @new_sal END CLOSE kurs DEALLOCATE kurs END ELSE BEGIN -- UPDATE DECLARE kurs CURSOR FOR SELECT empno, sal FROM inserted OPEN kurs FETCH NEXT FROM kurs INTO @empno, @new_sal WHILE @@FETCH_STATUS = 0 BEGIN IF @new_sal < 1000 BEGIN SELECT @old_sal = sal FROM deleted WHERE empno = @empno UPDATE emp set sal = @old_sal WHERE empno = @empno END FETCH NEXT FROM kurs INTO @empno, @new_sal END CLOSE kurs DEALLOCATE kurs END
W przykładzie tym następuje sprawdzenie, na podstawie liczby rekordów w tabelach INSERTED
i DELETED
, jaka instrukcja spowodowała uruchomienie wyzwalacza. W przypadku, gdy był to INSERT
, przechodzimy kursorem po tabeli INSERTED
i usuwamy wiersze, które nie spełniają warunku. W przypadku UPDATE
, również przechodzimy kursorem po tabeli INSERTED
, ale tym razem, dla niepoprawnych modyfikacji pobieramy ich starą, nadpisaną wartość i zmieniamy z powrotem na nią.
Zadanie to da się również wykonać bez użycia kursorów:
CREATE TRIGGER zarobki ON emp FOR INSERT, UPDATE AS DECLARE @ile INT SELECT @ile = COUNT(*) from deleted IF @ile = 0 BEGIN -- jeśli 0 rekordów w deleted, to był to INSERT DELETE FROM emp WHERE sal < 1000 AND empno IN (SELECT empno FROM inserted) END ELSE BEGIN -- UPDATE UPDATE emp SET sal = deleted.sal FROM inserted, deleted WHERE emp.empno = inserted.empno AND emp.empno = deleted.empno AND inserted.sal < 1000 END
Tym razem również sprawdzamy, która instrukcja spowodowała uruchomienie wyzwalacza. W przypadku INSERT
usuwamy rekordy o pensji mniejszej niż 1000, których identyfikatory znajdują się w tabeli INSERTED
, a więc te, które właśnie są wstawiane. W przypadku UPDATE
używamy skorelowanej instrukcji UPDATE
, która na bazie tabel INSERTED
i DELETED
zidentyfikuje i przywróci niepoprawnie zmodyfikowane dane. Skorelowany UPDATE
będzie omówiony dokładniej w wykładzie 15.
Dotychczasowe przykłady dotyczyły oprogramowania więzów spójności przy pomocy wyzwalaczy. Jak wspomniano na początku tego wykładu, wyzwalacze są też używane do automatyzacji stałych czynności w bazie danych. Kolejny przykład prezentuje tego typu użycie wyzwalacza.
W naszym przykładzie będzie istniała tabela "budzet" zawierająca sumaryczne zarobki wszystkich pracowników. Tabela będzie zawierała jeden wiersz i jedną kolumnę. Możemy ją utworzyć następującym poleceniem:
CREATE TABLE budzet (wartosc INT NOT NULL)
Wstawimy początkową wartość przy pomocy polecenia:
INSERT INTO budzet (wartosc) SELECT SUM(sal) FROM emp
Od tej chwili chcielibyśmy, aby wyzwalacz pilnował aktualności danych w tabeli „budzet”, czyli po każdym wstawieniu, usunięciu lub zmianie danych pracowników, wartość budżetu powinna być zaktualizowana. Rozwiązanie naiwne polegałoby na policzeniu sumy od początku wewnątrz wyzwalacza uruchamianego każdą instrukcją DML na tabeli „emp”. Byłoby to rozwiązanie bardzo mało wydajne. My natomiast policzymy różnicę pensji na podstawie tabel INSERTED
oraz DELETED
i zaktualizujemy budżet o tą różnicę:
CREATE TRIGGER akt_budzet ON emp FOR INSERT, UPDATE, DELETE AS DECLARE @zmniejsz INT, @zwieksz INT SELECT @zmniejsz = SUM(sal) FROM deleted SELECT @zwieksz = SUM(sal) FROM inserted UPDATE budzet SET wartosc = wartosc - ISNULL(@zmniejsz,0) + ISNULL(@zwieksz,0)
W serwerze Oracle zarówno składnia, jak i zasada działania wyzwalaczy, dość znacznie się różnią. Najważniejsze różnice:
BEFORE
lub AFTER
w jego nagłówku. Te pierwsze będą nadawały się lepiej do oprogramowania więzów spójności, a te drugie do automatyzacji.FOR EACH ROW
dzięki czemu dla każdego wiersza uruchomi się osobna instancja wyzwalacza. Jest to sytuacja dużo łatwiejsza dla programisty, gdyż pisząc kod może skupić się na obsłudze tylko jednego wiersza, a nie całej tabeli (jak INSERTED
i DELETED
).INSERTED
i DELETED
, lecz poprzez zmienne mające postać :NEW.nazwa_pola
i :OLD.nazwa_pola
. Są one dostępne wyłącznie w wyzwalaczach zadeklarowanych z dopiskiem FOR EACH ROW
.INSERTING
, UPDATING
i DELETING
, które określają, jaka instrukcja spowodowała uruchomienie wyzwalacza.Instrukcja tworząca wyzwalacz w PL/SQL ma następującą postać:
CREATE [OR REPLACE] TRIGGER nazwa {BEFORE / AFTER} specyfikacja instrukcji ON tabela [FOR EACH ROW] blok PL/SQL
Specyfikacja instrukcji może składać się z wielu instrukcji oddzielonych przez OR
, np. BEFORE INSERT OR UPDATE
. Dla UPDATE
możemy określić listę kolumn, których modyfikacja uruchomi wyzwalacz, np. UPDATE OF sal
.
Oto przykładowy wyzwalacz, który nie pozwoli usuwać rekordów z tabeli "salgrade":
CREATE OR REPLACE TRIGGER nie_usuwaj BEFORE DELETE ON salgrade BEGIN raise_application_error(-20500, 'Nie wolno usuwac z SALGRADE'); END;
A oto wyzwalacz pilnujący, aby wstawiana lub modyfikowana pensja nie mogła mieć wartości mniejszej niż 1000:
CREATE OR REPLACE TRIGGER pensja1000 BEFORE INSERT OR UPDATE OF sal ON emp FOR EACH ROW BEGIN IF :NEW.sal < 1000 THEN raise_application_error(-20500, 'Za mala pensja'); END IF; END;
Zwróćmy uwagę na klauzulę FOR EACH ROW
, dzięki której wyzwalacz uruchomi się wielokrotnie w przypadku, gdy modyfikacja dotyczy wielu wierszy. Jak widać, pozwala to napisać znacznie prostszy kod, niż w przypadku wyzwalacza uruchamianego raz dla całej instrukcji.
W kolejnym przykładzie pokazane jest rozwiązanie zadania dotyczącego tabeli "budżet" (ostatni przykład punktu 3).
CREATE OR REPLACE TRIGGER akt_budzet AFTER INSERT OR UPDATE OR DELETE ON emp FOR EACH ROW BEGIN IF INSERTING THEN UPDATE budzet SET wartosc = wartosc + :NEW.sal; ELSIF UPDATING THEN UPDATE budzet SET wartosc = wartosc + :NEW.sal - :OLD.sal; ELSIF DELETING THEN UPDATE budzet SET wartosc = wartosc - :OLD.sal; END IF; END;