一. DML觸發器; 二. DDL觸發器; 三. LOGON 觸發器; ...
觸發器可以理解為由特定事件觸發的存儲過程, 和存儲過程、函數一樣,觸發器也支持CLR,目前SQL Server共支持以下幾種觸發器:
1. DML觸發器, 表/視圖級有效,可由DML語句 (INSERT, UPDATE, DELETE) 觸發;
2. DDL 觸發器,資料庫級有效,可由DDL語句 (CREATE, ALTER, DROP 等) 觸發;
3. LOGON 觸發器, 實例級有效,可由用戶賬號登錄(LOGON)資料庫實例時觸發;
一. DML觸發器
1. 語句級觸發器/行級觸發器
在SQL Server中,從定義來說只有語句級觸發器,但如果有行級的邏輯要處理,有兩個僅在觸發器內有效的表 (inserted, deleted), 存放著受影響的行,可以從這兩個表裡取出特定的行並自行定義腳本處理;
在ORACLE中, 對錶做一次DML操作產生一次觸發,叫語句級觸發器,另外還可以通過指定[FOR EACH ROW]子句,對於表中受影響的每行數據均觸發,叫行級觸發器,原有行用:OLD表示,新行用:NEW表示;
2. BEFORE/AFTER/INSTEAD OF
在SQL Server中,從定義來說只有AFTER/INSTEAD OF觸發器,在表上支持AFTER觸發器,在表/視圖上支持INSTEAD OF觸發器,對於BEFORE觸發器的需求可以嘗試通過INSEAD OF觸發器來實現;
SQL Server DML Trigger |
BEFORE |
AFTER |
INSTEAD OF |
TABLE |
N/A |
√ |
√ |
VIEW |
N/A |
N/A |
√ |
在ORACLE中,在表上支持BEFORE/AFTER觸發器,在視圖上支持INSTEAD OF觸發器,比如ORACLE中無法直接對視圖做DML操作,可以通過INSTEAD OF觸發器來變樣完成;
ORACLE DML Trigger |
BEFORE |
AFTER |
INSTEAD OF |
TABLE |
√ |
√ |
N/A |
VIEW |
N/A |
N/A |
√ |
3. 觸發條件
(1) 不能觸發的情況
對於UPDATE,DELETE操作而言,均會觸發觸發器;而對於INSERT或者說IMPORT的情況,是可以控制不去觸發的。
- 大批量導入操作,如:BULK INSERT, bcp/INSERT... SELECT * FROM OPENROWSET,都有FIRE_TRIGGERS/IGNORE_TRIGGERS選項,可以設置是否觸發觸發器;
- 導入導出嚮導/SSIS,如果目標是表,也有FIRE_TRIGGERS的設置選項;
- 另外truncate操作也不會觸發;
(2) 嵌套觸發器 (Nested Triggers), 迴圈/遞歸觸發器 (Recursive Triggers)
嵌套觸發器,就是一次操作觸發了一個觸發器,然後觸發器里的語句繼續觸發其他觸發器,如果繼續回頭觸發了自己,那麼就是遞歸觸發器。
對於AFTER觸發器有個兩個開關分別控制嵌套觸發和遞歸觸發:
exec sp_configure 'nested triggers'
這個參數預設值為1, 也就是說允許AFTER觸發器嵌套,最多嵌套32層,設為0就是不允許AFTER觸發器嵌套,如下:
exec sp_configure 'nested triggers',0 RECONFIGURE
但這個參數有兩個另外:
- INSTEAD OF觸發器,可以嵌套,不受這個參數開關與否影響;
- AFTER觸發器,即使打開該選項,也不會自己嵌套自己(即遞歸),除非打開了RECURSIVE_TRIGGERS選項,也就是迴圈/遞歸觸發器;
--create table, sql server 2016 & higher drop table if exists A GO create table A(id int) GO --create DML trigger drop trigger if exists tri_01 GO create TRIGGER tri_01 ON A AFTER INSERT, UPDATE, DELETE as begin if @@NESTLEVEL = 32 begin return end insert A values(0) end GO --check nested triggers server option exec sp_configure 'nested triggers' --name minimum maximum config_value run_value --nested triggers 0 1 1 1 --test with RECURSIVE_TRIGGERS off ALTER DATABASE dba set RECURSIVE_TRIGGERS off select is_recursive_triggers_on, * from sys.databases GO insert A values(1) select * from A --id --1 --0 --test with RECURSIVE_TRIGGERS on ALTER DATABASE dba set RECURSIVE_TRIGGERS on select is_recursive_triggers_on, * from sys.databases GO truncate table A insert A values(1) select * from A --32 rows --如果沒有加@@NESTLEVEL判斷並退出,會出現32層限制的報錯,並且表裡不會插入任何數據 /* Msg 217, Level 16, State 1, Procedure tri_01, Line 10 Maximum stored procedure, function, trigger, or view nesting level exceeded (limit 32). select * from A --0 rows */ --刪表會級聯刪除觸發器,就像索引 drop table A
迴圈/遞歸觸發器的前提就是嵌套觸發器,只有允許嵌套了才可以遞歸(遞歸也就是嵌套並觸發自己),遞歸有直接和間接兩種情況:
- 直接遞歸:就是A表的DML觸發器再回來對A表進行DML操作,如上例;
- 間接遞歸:就是A表DML觸發器去操作B表,然後B表上觸發器回來操作A表,如下例;
--create table, sql server 2016 & higher drop table if exists A drop table if exists B GO create table A(id int) create table B(id int) GO --create DML trigger drop trigger if exists tri_01 drop trigger if exists tri_02 GO create TRIGGER tri_01 ON A AFTER INSERT, UPDATE, DELETE as begin if @@NESTLEVEL = 32 begin return end insert B values(0) end GO create TRIGGER tri_02 ON B AFTER INSERT, UPDATE, DELETE as begin if @@NESTLEVEL = 32 begin return end insert A values(0) end GO --test with nested triggers server option ON exec sp_configure 'nested triggers',1 RECONFIGURE --test with RECURSIVE_TRIGGERS off ALTER DATABASE dba set RECURSIVE_TRIGGERS off select is_recursive_triggers_on, * from sys.databases GO truncate table A truncate table B insert A values(1) select * from A --16 rows select * from B --16 rows --test with RECURSIVE_TRIGGERS on ALTER DATABASE dba set RECURSIVE_TRIGGERS on select is_recursive_triggers_on, * from sys.databases GO truncate table A truncate table B insert A values(1) select * from A --16 rows select * from B --16 rows --test with nested triggers server option OFF exec sp_configure 'nested triggers',0 RECONFIGURE --test with RECURSIVE_TRIGGERS off ALTER DATABASE dba set RECURSIVE_TRIGGERS off select is_recursive_triggers_on, * from sys.databases GO truncate table A truncate table B insert A values(1) select * from A --1 select * from B --0 --test with RECURSIVE_TRIGGERS on ALTER DATABASE dba set RECURSIVE_TRIGGERS on select is_recursive_triggers_on, * from sys.databases GO truncate table A truncate table B insert A values(1) select * from A --1 select * from B --0 --刪表會級聯刪除觸發器,就像索引 drop table A, B
- 可以看出資料庫選項RECURSIVE_TRIGGERS,僅對直接遞歸有效,對間接遞歸無效;可以通過Nest Triggers的開關來控制是否允許嵌套,從而控制是否允許間接遞歸;
- 不論直接遞歸,還是間接遞歸,遞歸次數都有32次嵌套的上限;
總結下來:
1. AFTER觸發器,預設Nest Triggers值為1,即允許觸發器嵌套,上限32層,間接遞歸也是可以的,直接遞歸需要開啟資料庫選項RECURSIVE_TRIGGERS;
2. INSTEAD OF觸發器,不受Nest Triggers選項影響,均可以嵌套,上限32層,間接遞歸也是可以的,直接遞歸無論是否開啟資料庫選項RECUSIVE_TRIGGERS,都無效;把上面兩個腳本示例中的AFTER改為INSTEAD OF即可演示。
4. 觸發器中無法commit/rollback事務
--create table, sql server 2016 & higher drop table if exists A GO create table A(id int) GO --create DML trigger drop trigger if exists tri_01 GO create TRIGGER tri_01 ON A AFTER INSERT, UPDATE, DELETE as begin if @@NESTLEVEL = 32 begin return end insert A values(0) commit end GO begin tran insert A values(1) /* Msg 3609, Level 16, State 1, Procedure tri_01, Line 10 The transaction ended in the trigger. The batch has been aborted. */
在SQL Server和Oracle中都是這樣,觸發器作為整個事務的一部分存在,但是並不控制整個事務的提交/回滾,為保證數據一致性,事務邏輯由觸發器外層的語句來控制。
二. DDL觸發器
SQL Server 2005開始支持DDL觸發器,它不只限於對CREATE/ALTER/DROP操作有效,支持的DDL事件還有比如:許可權的GRANT/DENY/REVOEK, 對象的RENAME, 更新統計信息等等,可通過DMV查看更多支持的事件類型如下:
select * from sys.trigger_event_types where type_name not like '%CREATE%' and type_name not like '%ALTER%' and type_name not like '%DROP%'
註意:
1. TRUNCATE不在DDL觸發器的事件類型中,SQL Server中將Truncate 歸為DML操作語句,雖然它也並不觸發DML觸發器,就像開啟開關的大批量導入操作 (Bulk Import Operations) 一樣;
2. DDL觸發器中捕獲的信息都由EVENTDATA()函數返回,返回類型為XML格式,需要用XQuery來讀取;
代碼示例1:記錄所有table上的某些DDL操作
--記錄所有create table操作 if OBJECT_ID('ddl_log','U') is not null drop table ddl_log GO create table ddl_log ( LogID int identity(1,1), EventType varchar(50), ObjectName varchar(256), ObjectType varchar(25), TSQLCommand varchar(max), LoginName varchar(256) ) GO if exists(select * from sys.triggers where name = 'TABLE_DDL_LOG' and parent_class_desc = 'DATABASE') drop trigger TABLE_DDL_LOG on database; GO create trigger TABLE_DDL_LOG on database for create_table as begin set nocount on declare @data xml set @data = EVENTDATA() insert into ddl_log values (@data.value('(/EVENT_INSTANCE/EventType)[1]', 'varchar(50)'), @data.value('(/EVENT_INSTANCE/ObjectName)[1]', 'varchar(256)'), @data.value('(/EVENT_INSTANCE/ObjectType)[1]', 'varchar(25)'), @data.value('(/EVENT_INSTANCE/TSQLCommand)[1]', 'varchar(max)'), @data.value('(/EVENT_INSTANCE/LoginName)[1]', 'varchar(256)') ) end GO drop table if exists test_dll_trigger; create table test_dll_trigger (id int) select * from ddl_log
代碼示例2:禁止特定角色的用戶對特定的表做DROP操作
IF exists(select * from sys.triggers where name = 'NO_DROP_TABLE' and parent_class_desc = 'DATABASE') DROP TRIGGER [NO_DROP_TABLE] ON DATABASE; GO CREATE TRIGGER NO_DROP_TABLE ON DATABASE FOR DROP_TABLE AS BEGIN DECLARE @x XML, @user_name varchar(100), @db_name varchar(100), @schema_name varchar(100), @object_name varchar(200) --select eventdata() SET @x = EVENTDATA(); SET @user_name = @x.value('(/EVENT_INSTANCE/UserName)[1]','varchar(100)'); SET @db_name = @x.value('(/EVENT_INSTANCE/DatabaseName)[1]','varchar(100)'); SET @schema_name = @x.value('(/EVENT_INSTANCE/SchemaName)[1]','varchar(100)'); SET @object_name = @x.value('(/EVENT_INSTANCE/ObjectName)[1]','varchar(100)'); --PRINT 'Current User: ' + @user_name --PRINT 'Current Database: ' + @db_name --PRINT 'Schema Name: ' + @schema_name --PRINT 'Table Name: ' + @object_name IF is_rolemember('disallow_modify_tables',@user_name) = 1 AND @db_name = 'YOUR_DB_NAME' AND @schema_name = 'YOUR_SCHEMA_NAME' AND @object_name like 'YOUR_TABLE_NAME%' BEGIN PRINT 'Dropping tables is not allowed' ROLLBACK END END GO
三. LOGON 觸發器
SQL Server 2005在SP2中悄悄引入了LOGON觸發器,作為一個實例級的對象,它的系統視圖,定義語句和DDL/DML觸發器都是分開的。
select * from sys.server_triggers where name = 'login_history_trigger' select * from sys.server_trigger_events select OBJECT_ID('login_history_trigger') --無法獲取
在SQL Server中,顧名思義,LOGON觸發器,只支持LOGON事件;
在ORACLE中,實例級觸發器可支持更多事件 (SERVERERROR, LOGON, LOGOFF, STARTUP, or SHUTDOWN)。
代碼示例1: 記錄所有login登錄歷史 (其實也可以通過修改login auditing選項,來記錄成功和失敗的登錄在errorlog里)
IF OBJECT_ID('login_history','U') is not null DROP TABLE login_history GO CREATE TABLE login_history ( FACT_ID bigint IDENTITY(1,1) primary key, LOGIN_NAME nvarchar(1024), LOGIN_TIME datetime ) GO IF EXISTS(select 1 from sys.server_triggers where name = 'login_history_trigger') DROP TRIGGER login_history_trigger ON ALL SERVER GO CREATE TRIGGER login_history_trigger ON ALL SERVER FOR LOGON AS BEGIN --IF SUSER_NAME() NOT LIKE 'NT AUTHORITY\%' AND -- SUSER_NAME() NOT LIKE 'NT SERVICE\%' IF ORIGINAL_LOGIN() NOT LIKE 'NT AUTHORITY\%' AND ORIGINAL_LOGIN() NOT LIKE 'NT SERVICE\%' BEGIN INSERT INTO DBA..login_history VALUES(ORIGINAL_LOGIN(),GETDATE()); END; END; GO --view login history after logon SELECT * FROM login_history
代碼示例2: 限制特定用戶在特定時間範圍登錄、限制連接數
--限制下班時間不能登錄 DROP TRIGGER IF EXISTS limit_user_login_time ON ALL SERVER GO CREATE TRIGGER limit_user_login_time ON ALL SERVER FOR LOGON AS BEGIN IF ORIGINAL_LOGIN() = 'TestUser' AND (DATEPART(HOUR, GETDATE()) < 9 OR DATEPART (HOUR, GETDATE()) > 18) BEGIN PRINT 'TestUser can only login during working hours!' ROLLBACK END END GO --限制連接數 DROP TRIGGER IF EXISTS limit_user_connections ON ALL SERVER GO CREATE TRIGGER limit_user_connections ON ALL SERVER WITH EXECUTE AS 'sa' FOR LOGON AS BEGIN IF ORIGINAL_LOGIN() = 'TestUser' AND (SELECT COUNT(*) FROM sys.dm_exec_sessions WHERE Is_User_Process = 1 AND Original_Login_Name = 'TestUser') > 2 BEGIN PRINT 'TestUser can only have 1 active session!' ROLLBACK END END
註意:如果LOGON觸發器把所有人都鎖在外面了怎麼辦?
Logon failed for login 'TestUser' due to trigger execution.
這時,只能通過DAC登錄SQL Server去禁用LOGON觸發器/修改邏輯以允許登錄,DAC登錄方式有遠程和本地兩種,遠程登錄需要通過sp_configure 開啟remote admin connections ,如果沒有事先開啟,那就只能選擇本地登錄方式:
伺服器本地,在SSMS中通過DAC登錄
伺服器本地,在cmd中通過DAC登錄
--禁用/啟用LOGON觸發器 DISABLE TRIGGER limit_user_connections ON ALL SERVER ENABLE TRIGGER limit_user_connections ON ALL SERVER
參考:
CREATE TRIGGER (Transact-SQL)
Create Nested Triggers
Transact-SQL statements
https://docs.microsoft.com/en-us/sql/t-sql/statements/statements?view=sql-server-2017
Why we can‘t use commit in trigger, can anyone give proper explanation
https://community.oracle.com/thread/1082134
Database PL/SQL Language Reference, Using Triggers
https://docs.oracle.com/cd/B28359_01/appdev.111/b28370/triggers.htm#LNPLS020