Wyzwalacze (ang. trigers) są specjalnym rodzajem procedur składowanych które są powiązane z językiem manipulowania danymi (DML) i jego zdarzeniami dotyczącymi tabel i widoków. Wyzwalacze nie mogą być wykonywane bezpośrednio. Wyzwalacze są wykonywane gdy zajdzie zdarzenie języka DML z którym są powiązane np INSERT, UPDATE lub DELETE. Wyzwalacze SQL Serwera działają w powiązaniu z dwoma rodzajami zdarzeń:
- Zdarzenia manipulacji danymi (DML trigers).
- Zdarzenia definicji danych (DDL trigers) np CREATE TABLE.
Wyzwalacze DML
Wyzwalacze DML są partiami kodu T-SQL powiązanymi z tabelą dla której chcemy przechwycić wystąpienie zdarzenia DML takiego jak INSERT, UPDATE lub DELETE lub ich kombinację. SQL Serwer pozwala na użycie dwóch rodzajów wyzwalaczy DML:
- AFTER - ten wyzwalacz jest wykonywany po wystąpieniu zdarzenia i może być zdefiniowany tylko dla stałych tabel.
- INSTEAD OF - ten wyzwalacz jest wykonywany zamiast zdarzenia i może być zdefiniowany dla stałych tabel i widoków.
Wyzwalacz wykonywany est tylko raz dla każdego zdarzenia DML, bez względu na to ile wierszy jest modyfikowanych. Schemat bazy danych wyzwalacza musi być taki sam jak schemat bazy danych tabeli lub widoku z którym jest powiązany. Wyzwalaczy można używać do audytu, egzekwowania skomplikowanej logiki integralności itp. Wycofanie zmian poprzez komendę ROLLBACK powoduje wycofanie wszystkich zmian jakie zostały dokonane przez wyzwalacz. Jednakże użycie ROLLBACK TRAN w wyzwalaczu może mieć pewne niepożądane wyniki. Zamiast tego zaleca się użycie instrukcji THROW lub RAISERROR i użycie standardowych procedur obsługi błędów. Wyjściem z wyzwalacza jest komenda RETURN. W obu typach wyzwalaczy można użyć tabeli zwanych INSERTED i DELETED. Tabele te zawierają wiersze które zostały zmodyfikowane poprzez zdarzenie które wywołało wyzwalacz. Tabela INSERTED zawiera nowe dane które zostały zmodyfikowane przez instrukcje INSERT lub UPDATE. Tabela DELETED zawiera obraz starych danych które zostały zmodyfikowane przez instrukcje UPDATE lub DELETE.
Wyzwalacz AFTER
Wyzwalacze AFTER mogą być definiowane jedynie dla tabel. W tych wyzwalaczach kod jest wykonywany po tym jak instrukcja języka DML spełni wszystkie ograniczenia nałożone na tabelę jak np ograniczenie klucza obcego lub unikalność. Jeżeli ograniczenie jest naruszone to instrukcja nie zostanie wykonana i wyzwalacz nie zostanie uruchomiony. Przykładowy wyzwalacz AFTER dla tabeli Sales.OrderDetails zwracający ilość modyfikowanych wierszy:
IF OBJECT_ID('Sales.tr_SalesOrderDetailsDML', 'TR') IS NOT NULL DROP TRIGGER Sales.tr_SalesOrderDetailsDML; GO CREATE TRIGGER Sales.tr_SalesOrderDetailsDML ON Sales.OrderDetails AFTER DELETE ,INSERT ,UPDATE AS BEGIN IF @@ROWCOUNT = 0 RETURN; SET NOCOUNT ON; SELECT COUNT(*) AS InsertedCount FROM Inserted; SELECT COUNT(*) AS DeletedCount FROM Deleted; END;
Wyzwalacz tworzymy używając instrukcji CREATE TRIGGER, po czym podajemy jego nazwę i po wyrażeniu ON wybieramy tabelę z którą ma być powiązany. Po wyborze tabeli deklarujemy typ wyzwalacza jako AFTER lub INSTEAD OF. Typ AFTER zostanie wybrany również jeżeli napiszemy FOR. Następnie deklarujemy dla jakich operacji języka DML ma być uruchamiany wyzwalacz po czym deklarujemy ciało wyzwalacza. Kiedy wykonana została operacja INSERT, UPDATE lub DELETE ale żadne wiersze nie zostały zmodyfikowane to nie ma potrzeby uruchamiać wyzwalacza. Można poprawić wydajność wyzwalacza poprzez sprawdzenie czy rekordy zostały zmodyfikowane. Można dokonać tego poprzez sprawdzenie wartości zmiennej globalnej @@ROWCOUNT na samym początku ciała wyzwalacza. Jeżeli @@ROWCOUNT jest równe zero to opuszczamy wyzwalacz komendą RETURN. Zwracanie wyników z wyzwalaczy nie jest dobrą praktyką. Można wyłączyć tę możliwość poprzez procedurę sp_configure i zmianę właściwości Disallow Results From Triggers. Utwórzmy wyzwalacz który będzie wykonywał jakąś pracę. Tabela Production.Categories nie posiada ograniczenia unikalności na kolumnie categoryname. Poniższy kod wymusi unikalność za pomocą wyzwalacza:
IF OBJECT_ID('Production.tr_ProductionCategories_categoryname', 'TR') IS NOT NULL DROP TRIGGER Production.tr_ProductionCategories_categoryname; GO CREATE TRIGGER Production.tr_ProductionCategories_categoryname ON Production.Categories AFTER INSERT ,UPDATE AS BEGIN IF @@ROWCOUNT = 0 RETURN; SET NOCOUNT ON; IF EXISTS ( SELECT COUNT(*) FROM Inserted AS I JOIN Production.Categories AS C ON I.categoryname = C.categoryname GROUP BY I.categoryname HAVING COUNT(*) > 1 ) BEGIN THROW 50000 ,'Duplicate category names not allowed' ,0; END; END; GO
Przetestujmy wyzwalacz za pomocą wyrażenia:
INSERT INTO Production.Categories (categoryname,description) VALUES ('TestCategory1', 'Test1 description v1');
Komenda INSERT zadziała za pierwszym razem ponieważ kategoria o podanej nazwie nie istnieje. Przy kolejnej próbie wykonania kodu otrzymamy w rezultacie błąd:
Msg 50000, Level 16, State 0, Procedure tr_ProductionCategories_categoryname, Line 17 Duplicate category names not allowed
Zagnieżdżone wyzwalacze AFTER
Wyzwalacze AFTER mogą być zagnieżdżone. Przykładowo mamy wyzwalacz na tabeli A który aktualizuje tabelę B. Tabela B posiada również wyzwalacz. Maksymalna liczba zagnieżdżeń wyzwalaczy to 32. Kiedy zagnieżdżenia tworzą pętlę to po 32 zagnieżdżeniu wykonywanie wyzwalaczy zostanie zatrzymane. Wykonywanie zagnieżdżonych wyzwalaczy w SQL Serwerze można wyłączyć używając procedury sp_configure:
EXEC sp_configure 'nested triggers', 0; RECONFIGURE;
Wyzwalacze INSTEAD OF
Wyzwalacze INSTEAD OF wykonują partię kodu T-SQL zamiast instrukcji INSERT, UPDATE lub DELETE. Wyzwalacze INSTEAD OF mogą być tworzone dla tabel i widoków, najczęściej używa się ich w widokach. Powodem jest to, że kiedy używamy komendy UPDATE na widoku to tylko jedna tabela może być aktualizowana jednocześnie. Wyzwalacz INSTEAD OF może być użyty dla komendy UPDATE na widoku i zamiast wykonywać aktualizację zastąpić ją aktualizacją więcej niż jednej tabeli widoku. Przykładowo przeróbmy wyzwalacz z poprzedniej sekcji na wyzwalacz INSTEAD OF dla instrukcji INSERT:
IF OBJECT_ID('Production.tr_ProductionCategories_categoryname', 'TR') IS NOT NULL DROP TRIGGER Production.tr_ProductionCategories_categoryname; GO CREATE TRIGGER Production.tr_ProductionCategories_categoryname ON Production.Categories INSTEAD OF INSERT AS BEGIN SET NOCOUNT ON; IF EXISTS (SELECT COUNT(*) FROM Inserted AS I JOIN Production.Categories AS C ON I.categoryname = C.categoryname GROUP BY I.categoryname HAVING COUNT(*) > 1 ) BEGIN THROW 50000, 'Duplicate category names not allowed', 0; END; ELSE INSERT Production.Categories (categoryname, description) SELECT categoryname, description FROM Inserted; END; GO
Funkcje wyzwalaczy DML
Można używać dwóch funkcji w wyzwalaczach do otrzymania informacji na temat zmian jakie zaszły:
- UPDATE() - można użyć yej funkcji aby dowiedzieć się czy dana kolumna była używana w komendzie INSERT lub UPDATE. Przykładowo można użyć jej tak w wyzwalaczu:
IF UPDATE(qty) PRINT 'Column qty affected';
Funkcja ta zwróci TRUE jeżeli kolumna była używana w komendzie UPDATE lub INSERT. - COLUMN_UPDATED() - można użyć tej funkcji znając sekwencję kolumn w tabeli. Wymaga ona użycia operatora AND (&) do podglądu jakie kolumny były aktualizowane.
Ćwiczenia
I. Kontrola zawartości tabel INSERTED i DELETED
- Stwórz wyzwalacz dla tabeli Sales.OrderDetails jak poniżej:
USE TSQL2012 GO IF OBJECT_ID('Sales.tr_SalesOrderDetailsDML', 'TR') IS NOT NULL DROP TRIGGER Sales.tr_SalesOrderDetailsDML; GO CREATE TRIGGER Sales.tr_SalesOrderDetailsDML ON Sales.OrderDetails AFTER DELETE, INSERT, UPDATE AS BEGIN IF @@ROWCOUNT = 0 RETURN; SET NOCOUNT ON; SELECT COUNT(*) AS InsertedCount FROM Inserted; SELECT COUNT(*) AS DeletedCount FROM Deleted; END;
Spróbuj usunąć nieistniejące rekordy z tabeli
Sales.OrderDetails
- :
DELETE FROM Sales.OrderDetails WHERE orderid = 10249 and productid in (15, 16); GO
- Dodaj dane do tabeli
Sales.OrderDetails
- :
INSERT INTO Sales.OrderDetails (orderid,productid,unitprice,qty,discount) VALUES (10249, 16, 9.00, 1, 0.60) , (10249, 15, 9.00, 1, 0.40); GO
- Zaktualizuj jeden z poprzednio dodanyc wierszy:
UPDATE Sales.OrderDetails SET unitprice = 99 WHERE orderid = 10249 AND productid = 16; GO
- Usuń dodane wiersze:
DELETE FROM Sales.OrderDetails WHERE orderid = 10249 and productid in (15, 16);
- Usuń wyzwalacz:
IF OBJECT_ID('Sales.tr_SalesOrderDetailsDML', 'TR') IS NOT NULL DROP TRIGGER Sales.tr_SalesOrderDetailsDML; GO
II. Użycie wyzwalaczy AFTER do obsługi logiki biznesowej
- napisz wyzwalacz który będzie wykonywał następujące czynności: jeżeli jakikolwiek wiersz z tabeli Sales.OrderDetails posiada cenę jednostkową mniejszą niż 10 to nie może posiadać rabatu większego niż 0.5:
USE TSQL2012; GO -- Step 1: Basic trigger IF OBJECT_ID('Sales.OrderDetails_AfterTrigger', 'TR') IS NOT NULL DROP Trigger Sales.OrderDetails_AfterTrigger; GO CREATE TRIGGER Sales.OrderDetails_AfterTrigger ON Sales.OrderDetails AFTER INSERT, UPDATE AS BEGIN IF @@ROWCOUNT = 0 RETURN; SET NOCOUNT ON; -- Perform the check DECLARE @unitprice AS money, @discount AS NUMERIC(4,3); SELECT @unitprice = unitprice FROM inserted; SELECT @discount = discount FROM inserted; IF @unitprice < 10 AND @discount > .5 BEGIN THROW 50000, 'Discount must be < = .5 when unitprice < 10', 0; END; END; GO
- Przetestuj wyzwalacz na dwóch wierszach z których pierwszy nie spełnia warunku wyzwalacza:
INSERT INTO Sales.OrderDetails (orderid,productid,unitprice,qty,discount) VALUES (10249, 16, 9.00, 1, 0.60) , (10249, 15, 9.00, 1, 0.40);
- Przetestuj wyzwalacz na dwóch wierszach z których drugi nie spełnia warunku wyzwalacza:
INSERT INTO Sales.OrderDetails (orderid,productid,unitprice,qty,discount) VALUES (10249, 15, 9.00, 1, 0.40), (10249, 16, 9.00, 1, 0.60) ;
- Usuń złe wiersze:
DELETE FROM Sales.OrderDetails WHERE orderid = 10249 AND productid IN (15, 16); GO
- Popraw wyzwalacz, tak aby sprawdzał wszystkie wiersze:
IF OBJECT_ID('Sales.OrderDetails_AfterTrigger', 'TR') IS NOT NULL DROP Trigger Sales.OrderDetails_AfterTrigger; GO CREATE TRIGGER Sales.OrderDetails_AfterTrigger ON Sales.OrderDetails AFTER INSERT, UPDATE AS BEGIN IF @@ROWCOUNT = 0 RETURN; SET NOCOUNT ON; -- Check all rows IF EXISTS(SELECT * FROM inserted AS I WHERE unitprice < 10 AND discount > .5) BEGIN THROW 50000, 'Discount must be < = .5 when unitprice < 10', 0; END END GO
- Przetestuj wyzwalacz na dwóch wierszach z których drugi nie spełnia warunku wyzwalacza:
INSERT INTO Sales.OrderDetails (orderid,productid,unitprice,qty,discount) VALUES (10249, 15, 9.00, 1, 0.40), (10249, 16, 9.00, 1, 0.60) ;
- Usuń wyzwalacz:
IF OBJECT_ID('Sales.OrderDetails_AfterTrigger', 'TR') IS NOT NULL DROP Trigger Sales.OrderDetails_AfterTrigger; GO
Podsumowanie
- Wyzwalacze DML są partiami kodu T-SQL podobnymi do procedur składowanych powiązanymi z tabelami i widokami. Można użyć wyzwalaczy do audytu, egzekwowania skomplikowanych zasad integralności, dodatkowej logiki biznesowej itp.
- Wyzwalacze są uruchamiane po wystąpieniu zdarzeń DML takich jak INSERT, UPDATE lub DELETE.
- T-SQL pozwala na użycie dwóch rodzajów wyzwalaczy DML: AFTER i INSTEAD OF.
- W obu typach można używać tabel INSERTED i DELETED.