觸發器可以做很多事情,但也會帶來很多問題。正確的使用在於在適當的時候使用,而不要在不適當的時候使用它們。
觸發器的一些常見用途如下:
[1] 彈性參照完整性:實現很多DRI不能實現的操作(例如,跨數據庫或服務器的參照完整性以及很多復雜的關系類型)。
[2] 創建審計跟蹤:這意味寫出的記錄不僅跟蹤大多數當前的數據,還包括對每個記錄進行實際修改的歷史數據。隨着SQL Server2008中的更改數據跟蹤功能的出現,創建審計跟蹤不再那么流行,但以前使用的就是觸發器。
[3] 執行與CHECK約束類似的功能,但是跨表,跨數據庫甚至是跨服務器使用。
[4] 用自己的語句代替用戶的操作語句。
一、定義概念
觸發器是一種特殊類型的存儲過程,對特定事件作出相應。
1、觸發器有兩種類型:數據定義語言(DDL)觸發器和數據操縱語言(DML)觸發器。
DDL觸發器在用戶以某些方式(CREATE、ALTER、DROP或相似的語句)對數據庫結構進行修改時激活作出響應。一般來說,只會在對數據庫結構的改變或歷史進行極為嚴格的審計時才會用到DDL觸發器。
DML觸發器是一些附加在特定表或視圖上的代碼片段。與需要顯式調用代碼的存儲過程不同,只要有附加觸發器的事件在表中發生,觸發器中的代碼就會自動運行。實際上也不能顯式地調用觸發器,唯一的做法是在指定的表中執行所需的操作。
除了不能夠顯式地調用觸發器,還可在存儲過程中發現另外兩個觸發器所沒有的內容:參數和返回碼。
因為在SQL中可以使用3類動作查詢,所以就有3種類型的DML觸發器,另外加上混合搭配這些事件並對事件定時激活的混合觸發器類型。
[1] INSERT觸發器
[2] DELETE觸發器
[3] UPDATE觸發器
[4] 以上任意類型的混合
注意:
有時即使執行的動作是前面這些類型中的一種,觸發器也不會激活。問題在於進行的操作是否在記錄的活動中。例如,DELETE語句是一個正常的記錄活動,它會激活任何刪除觸發器,而TRUNCATE TABLE也有刪除行的作用,但只是把表使用的空間釋放而已,沒有記錄單個行刪除操作,所以沒有激活任何觸發器。批量操作默認情況下不激活觸發器,需要顯式告知批量操作激活觸發器。
2、創建
創建觸發器的語法:
CREATE TRIGGER <trigger name>
ON [ <schema name>. ]<table or view name> [WITH ENCRYPTION | EXECUTE AS <CALLER | SELF | <user> > ] {{{ FOR | AFTER} < [DELETE][,][INSERT][,][UPDATE] > } | INSTEAD OF }[WITH APPEND][NOT FOR REPLICATION] AS < <sql statements> | EXTERNAL NAME <assembly method specifier> >
ON子句用來指定觸發器將要附加的表,以及在何時何種情況下激活這個觸發器。
1、ON子句
這部分只是對創建觸發器所針對的對象進行命名。記住,如果觸發器的類型是AFTER觸發器(使用FOR或AFTER來聲明觸發器),那么ON子句的目標就必須是一個表,AFTER觸發器不支持視圖。
2、WITH ENCRYPTION選項
加密觸發器。如果添加了這個選項,則可以確保沒有人能夠查看你的代碼(甚至是你自己)。和視圖與存儲過程一樣,使用WITH ENCRYPTION選項需要記住的是,每次在觸發器上使用ALTER語句時都必須重新應用該選項,如果使用ALTER STATEMENT語句但不包含WITH ENCRYPTION選項,那么觸發器就不再被加密。
3、FOR|AFTER子句與INSTEAD OF子句
除了要確定激活觸發器(INSERT、UPDATE、DELETE)的查詢類型以外,還要對觸發器的激活時間做出選擇。雖然人們經常考慮使用FOR觸發器,但是也可以使用INSTEAD OF觸發器。對這兩個觸發器的選擇將會影響到是在修改數據之前還是之后進入觸發器。FOR和AFTER的意義是一樣的。
(1) FOR|AFTER
FOR(或者AFTER)子句表明了期望觸發器在何種動作類型下激活。當有INSERT、UPDATE或DELETE或三者混合操作時,都可以激活觸發器。
FOR INSERT,DELETE --或者是: FOR UPDATE,INSERT --或者是: FOR DELETE
[1] INSERT觸發器
當有人向表中插入新的一行時,被標記為FOR INSERT的觸發器的代碼就會執行。對於插入的每一行來說,SQL Server會創建一個新行的副本並把該副本插入到一個特殊的表中,該表只在觸發器的作用域內存在,該表被稱為Inserted表。特別需要注意的是,Inserted表只在觸發器激活時存在。在觸發器開啟之前或完成之后,都要認為該表示不存在的。
[2] DELETE觸發器
它和INSERT觸發器的工作方式相同,只是Inserted表示空的(畢竟是進行刪除而非插入,所以對於Inserted表示沒有記錄)。相反,每個被刪除的記錄的副本將會插入到另一個表中,該表稱為Deleted表,和Inserted表類似,該表只存在於觸發器激活的時間內。
[3] UPDATE觸發器
除了有一點改變以外,UPDATE觸發器和前面的觸發器是很類似的。對表中現有的記錄進行修改時,都會激活被聲明FOR UPDATE的觸發器的代碼。唯一的改變是沒有UPDATE表。SQL Server認為每一行好像刪除了現有記錄,並插入了全新的記錄。聲明為FOR UPDATE的觸發器並不是只包含一個表,而是兩個特殊的表,稱為Inserted表和Deleted表。當然,這兩個表的行數是完全相同。
4、WITH APPEND選項
WITH APPEND選項並不常用,老實講,用到它的可能性很小;WITH APPEND選項只能應用於6.5兼容模式中。
如果已經聲明了一個稱為trgCheck的觸發器在更新和插入時強制執行數據完整性,那么就不能創建另一個觸發器來進行級聯更新。一旦創建了更新(或插入、刪除)觸發器,那么就不能創建另一個同一動作類型的觸發器。為解決這個問題,WITH APPEND子句顯式地告訴SQL Server,即使在表上已經有了這種類型的觸發器,還可以添加一個新的觸發器。當有合適的觸發動作(INSERT、UPDATE、DELETE)發生時,會同時激活兩個觸發器。
5、NOT FOR REPLICATION選項
如果添加了該選項,會稍微地改變關於何時激活觸發器的規則。在適當的位置使用這個選項,無論與復制相關的任務何時修改表,都不會激活觸發器。通常,當修改了原始表,並且不會再進行修改的時候會激活觸發器(進行內務處理或級聯等操作)。
6、AS子句
和在存儲過程中的使用完全相同,這正是觸發器的實質所在。AS關鍵字告訴SQL Server,代碼將要啟動。
二、使用觸發器實施數據完整性規則
雖然觸發器不會成為首要的選擇,但是觸發器也同樣可以執行和CHECK約束甚至是DEFAULT約束一樣的功能。看情況而定。如果CHECK約束可以完成,那么可能CHECK約束是更受青睞的選擇。但是,有時會出現CHECK約束不能完成任務的情況,或是CHECK過程中的某些固有內容使其顯得不如觸發器更為可取。
想要使用觸發器而非CHECK約束的例子包括:
[1] 業務規則需要引用單個表中的數據。
[2] 業務規則需要檢查更新的變化。
[3] 需要一個定制的錯誤消息。
1、處理來自於其他表的需求
CHECK約束快速而且有效,但是他們不是萬能的。可能當你需要跨表驗證時,它最大的缺點就會暴露出來。
為了演示一次跨表約束,本處新建兩個表用於測試:
此處外鍵列是ProductId。此處我們要測試的是,當產品表的PruductNumber(庫存,單詞不懂寫)小於等於0的時候,不允許再添加1產品的訂單。
下面創建一個觸發器如下:
CREATE TRIGGER ProductNumCheck ON [Order] FOR INSERT AS DECLARE @i int SELECT @i = ProductId FROM Inserted --Inserted表示最后插入的記錄的表 IF(SELECT ProductNumber FROM Product WHERE ProductId = (SELECT ProductId FROM Inserted)) <=0 PRINT @i BEGIN PRINT '庫存不足,禁止購買!' ROLLBACK TRANSACTION --回滾,避免插入 END
2、使用觸發器檢查更新的變化
有時,我們並不關心過去的值和現在的值,而是關注變化的量。雖然沒有任何列或表給出這些信息,但是可以在觸發器中使用Inserted表和Deleted表進行計算。
例如,剛才的產品表,假設在下訂單時會修改產品的庫存,我們不允許一次UPDATE Product超過10個。
CREATE TRIGGER ProductNumUpdate ON Product FOR UPDATE AS IF EXISTS(SELECT * FROM Inserted AS i INNER JOIN Deleted as d ON i.ProductId = d.ProductId WHERE i.ProductNumber - d.ProductNumber > 10) BEGIN PRINT '超過10個,不允許更新'; ROLLBACK TRANSACTION --回滾,避免插入 END
3、將觸發器用於自定義錯誤消息
當想要對傳給用戶或客戶端應用程序的錯誤消息或錯誤號進行控制時,使用觸發器是很方便的。
例如,如果使用CHECK約束,只能得到標准的547錯誤,並且沒有詳盡的解釋。通常,對於想知道具體錯誤的用戶來說,這是無用的信息-缺失,客戶端應用程序經常因為沒有足夠的信息而不能代表用戶做出有幫助的響應。
簡而言之,當已經具備了數據完整性,但是沒有足夠的信息進行處理的時候,可以創建觸發器。
注意:
盡管傳遞自定義錯誤代碼很有用,但SQL Server中對自定義錯誤消息的需求還是相對較少。為什么不傳遞自定義錯誤消息呢?原因在於某些用戶認為自定義錯誤消息之上有一個應用程序層,並且可能需要更多有關錯誤的上下文信息,因此特定於SQL Server的文本就無法充分發揮作用。而這時如果使用特定的錯誤代碼,對於應用程序則有很大幫助,有助於確定確切發生的事件以及應用正確的客戶端錯誤處理代碼。
三、觸發器的常見用途
1、觸發器可以嵌套
嵌套的觸發器是指那些不是由發出語句直接激活的,而是由另一個觸發器發出的語句激活的觸發器。
這實際上會引起一連串的事件,一個觸發器激活另一個觸發器,而另一個觸發器又激活其他觸發器。
觸發器可以激活的深度取決於以下幾個因素:
[1] 嵌套的觸發器是否已在系統中打開(這是系統級的而不是數據庫級的選項;可以使用sp_configure來設置,默認為打開的)。
[2] 是否有嵌套的深度不超過32層。
[3] 觸發器是否已經被激活。觸發器默認為每個觸發器事務只能被激活一次。一旦被激活,則觸發器會忽略其他任何調用,將這些調用作為相同觸發器動作的一部分。一旦執行一條全新的語句,處理過程就會重新開始。
注意,如果在嵌套鏈中的任何地方進行了ROLLBACK操作,那么整條鏈都會回滾。換句話說,整個觸發器鏈就像一個事務一樣。
2、觸發器可以遞歸
如果某觸發器所做的事情最終激活了自身,那么該觸發器就是遞歸的。可以直接觸發(通過設置了觸發器的表進行動作查詢來完成),也可以間接觸發(通過嵌套過程)。
遞歸觸發器比較少見,默認情況下,遞歸觸發器是關閉的。遞歸是數據庫級的選項,可以使用sp_dboption系統存儲過程來設置。
遞歸觸發器的風險在於可能會陷入某種非預設的循環之中。這樣便需要確保在必要的時候可以通過遞歸檢查的形式來停止這一過程。
3、觸發器不能防止體系結構的修改
觸發器有助於更容易地修改體系結構。事實上,通常在開發周期的早期使用觸發器實施參照完整性,而在后期,也就是要進入生產環境時將其改為DRI。
4、可以在不刪除的情況下關閉觸發器
有時,像CHECK約束一樣,你想要關閉完整性功能以便於執行一些違反約束但是有效的動作(最常見的就是導入數據)。
可以使用ALTER語句來關閉觸發器,語法如下:
ALTER TABLE <table name> <ENABLE|DISABLE> TRIGGER <ALL|<trigger name>>
如果關閉觸發器是為了導入數據,那么建議踢出所有用戶並進入單用戶模式。dbo-only模式,或同時進入兩種模式。這樣一來,當關閉觸發器時,就能確保萬無一失。
5、觸發器的激活順序
對於任何給定的表(只有AFTER觸發器才可以指定激活順序),給定的視圖(只有INSTEAD OF觸發器才可以指定激活順序)。可以選擇一個觸發器優先激活(FIRST唯一一個)。同樣,可以選擇一個觸發器最后激活(LAST,只能選一個)。其他所有的觸發器之間沒有什么優先激活順序,也就是說,除了能保證FIRST第一個觸發和LAST最后激活之外,不能保證NONE觸發器的順序。
FIRST和LAST觸發器的創建和其他任何觸發器的創建相同,在已經創建觸發器之后使用存儲過程sp_settriggerorder來聲明激活順序。
sp_settriggerorder語法如下:
sp_settriggerorder[@triggername =] '<trigger name>', [@order =] '{FIRST|LAST|NONE}', [@stmttype =] '{INSERT|UPDATE|DELETE}' [, [@namespace =] {'DATABASE'|'SERVER'|NULL}]
這里對於任何特殊操作(INSERT、UPDATE、DELETE)來說,只能有唯一的FIRST觸發器。同樣,對於任何特殊操作來說,也只能有唯一的LAST觸發器。其他觸發器的數量可以看做是NONE,也就是說,沒有特殊激活順序的觸發器的數量是沒有限制的。
控制激活順序的意義:
[1] 出於邏輯原因而控制激活順序
最常見的理由是第一個觸發器是后面觸發器的基礎或前面的觸發器使后面的觸發器有效。
[2] 處於性能原因而控制激活順序
在性能方面,FIRST觸發器是唯一起關鍵作用的觸發器,如果有多個觸發器,但是其中只有一個觸發器可能會產生回滾,那么就需要考慮將這個觸發器標記為FIRST觸發器,這能令外回滾的操作更少。
四、性能考慮
1、觸發器的被動型
這里是指觸發器發生在事務之后。當激活觸發器時,整個查詢已經運行並且事務也已經被記錄到日志中(但未提交,只是記錄到激活觸發器的語句點)。如果觸發器需要回滾,那么必須撤銷已經做的所有工作。這和約束是不同,約束是主動的,約束是發生在語句真正執行前。這意味着約束會檢測可能失敗的操作,並且在進程的前期就予以阻止。所以約束通常運行得快一些-在更為復雜的查詢中速度更快。注意,只有在發生回滾時,約束明顯更快。
如果正在處理少量回滾,而且受影響的語句的復雜性較低,執行之間較短,那么觸發器和約束之間沒有太大的區別。但是在無法預知回滾的數量的時候,堅持使用約束的效率更好。
2、觸發器與激活的進程之間不存在並發問題
如果激活語句不是顯示事務的一部分,那么該語句仍然是其自身的語句事務的一部分。無論何種情況,觸發器內部發出的ROLLBACK TRAN仍然會回滾整個事務。
這種同屬一個事務的另一個結果是觸發器繼承了他們所屬事務上已打開的鎖。這意味着不需要做任何特殊的處理來避免碰到事務中其他語句創建的鎖。在事務的作用域內可以自由訪問,並且可以發現數據庫基於事務中先前的語句所作的修改。
3、使用IF UPDATE()和COLUMNS_UPDATE()
在UPDATE觸發器中,可以通過檢查相關的列是否已被修改來限制在觸發器中執行的代碼總量。為了實現這一點,可以使用UPDATE()或COLUMN_UPDATE()函數。
1、UPDATE()函數
UPDATE()函數只在觸發器的作用域內適用。它唯一的目的是提供一個布爾值,來說明特殊列是否已經更新。使用這個函數可以決定一個特定的代碼塊是否需要運行-例如該代碼只在特定列更新時才運行。
建一張表如下:
創建觸發器如下:
CREATE TRIGGER UPDATECHECK ON tb_Money FOR UPDATE AS IF UPDATE(MyMoney) --如果更新了MyMoney才觸發 BEGIN PRINT('我的錢改變了!'); END
執行語句:
UPDATE tb_Money SET MyMoney = '101' WHERE Id = 1 --改變了MyMoney激活了觸發器
輸出如下:
留意到,改變了MyMoney列,激活了觸發器。
執行語句:
UPDATE tb_Money SET Name = '張飛' WHERE Id = 1
顯示結果如下:
2、COLUMNS_UPDATE()函數
這個函數和UPDATE()的運行方式不同,但目的相同。COLUMNS_UPDATE()函數可以一次檢查多列。為了實現這一點,該函數使用了位掩碼,位掩碼將varbinary數據的一個或多個字節中的單個位與表中的單個列相關聯。
對於上圖的情況,數據的單個字節說明了第2,第3,以及第6列已經更新,而其他列沒有更新。
對於超過8列的情況,SQL Server就會在右邊添加另一個字節並且繼續計數。
對於上圖,這次是更新了第2,第9以及第14列。
這些信息使用方法:
| 表示 或
& 表示 與
^ 表示 異或
示例:
COLUMN_UPDATE()>0 檢查是否有列被更新。
COLUMN_UPDATE()^21=0 檢查是否更新了所有指定列(1、3、5)。
還是剛才那張表:
創建觸發器如下:
CREATE TRIGGER UPDATECHECK2 ON tb_Money FOR UPDATE AS IF COLUMNS_UPDATED()&7 = 3 --如果同時更新了Name,MyMoney才觸發 BEGIN PRINT('我的錢和姓名改變了!'); END
執行語句以及說明如下:
UPDATE tb_Money SET Name = '張飛' WHERE Id = 1 UPDATE tb_Money SET Name = '趙雲', MyMoney = 102 WHERE Id = 1 --此行會激活觸發器 --計算過程如下 --Id Name tb_Money --1 1 1 7(全部更新為7) --0 1 1 Name和tb_Money同時更新為(與3=3)
5、盡量別在觸發器中回滾
如果在觸發器中使用很多的ROLLBACK TRAN語句,那么請確保在執行激活觸發器的語句前預先進行錯誤檢查。SQL Server在這種情況下,是被動的,但也可以主動。時間檢查錯誤,而不是等待回滾。回滾的代價是昂貴的。
五、刪除觸發器
刪除觸發器和普通刪除操作略有不同,和表一樣,其問題在於觸發器的名稱被限定在模式級別。這意味着一個觸發器可以有兩個名稱相同的對象,只要方式觸發器的對象與觸發器另一個同名的對象位於不同的模式中。重申一次,觸發器是以其所處的模式命名的,而不是以觸發器所關聯的對象命名。
刪除觸發器的語法如下:
DROP TRIGGER [<schema>.]<trigger name>
除了模式問題之外,刪除觸發器就和刪除其他對象一樣簡單了。