触发器能够理解为由特定事件触发的存储过程, 和存储过程、函数同样,触发器也支持CLR,目前SQL Server共支持如下几种触发器:sql
1. DML触发器, 表/视图级有效,可由DML语句 (INSERT, UPDATE, DELETE) 触发;数据库
2. DDL 触发器,数据库级有效,可由DDL语句 (CREATE, ALTER, DROP 等) 触发;服务器
3. LOGON 触发器, 实例级有效,可由用户帐号登陆(LOGON)数据库实例时触发;session
一. DML触发器oracle
1. 语句级触发器/行级触发器app
在SQL Server中,从定义来讲只有语句级触发器,但若是有行级的逻辑要处理,有两个仅在触发器内有效的表 (inserted, deleted), 存放着受影响的行,能够从这两个表里取出特定的行并自行定义脚本处理;ide
在ORACLE中, 对表作一次DML操做产生一次触发,叫语句级触发器,另外还能够经过指定[FOR EACH ROW]子句,对于表中受影响的每行数据均触发,叫行级触发器,原有行用:OLD表示,新行用:NEW表示;函数
2. BEFORE/AFTER/INSTEAD OFspa
在SQL Server中,从定义来讲只有AFTER/INSTEAD OF触发器,在表上支持AFTER触发器,在表/视图上支持INSTEAD OF触发器,对于BEFORE触发器的需求能够尝试经过INSEAD OF触发器来实现;code
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的状况,是能够控制不去触发的。
(2) 嵌套触发器 (Nested Triggers), 循环/递归触发器 (Recursive Triggers)
嵌套触发器,就是一次操做触发了一个触发器,而后触发器里的语句继续触发其余触发器,若是继续回头触发了本身,那么就是递归触发器。
对于AFTER触发器有个两个开关分别控制嵌套触发和递归触发:
exec sp_configure 'nested triggers'
这个参数默认值为1, 也就是说容许AFTER触发器嵌套,最多嵌套32层,设为0就是不容许AFTER触发器嵌套,以下:
exec sp_configure 'nested triggers',0 RECONFIGURE
但这个参数有两个另外:
--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
循环/递归触发器的前提就是嵌套触发器,只有容许嵌套了才能够递归(递归也就是嵌套并触发本身),递归有直接和间接两种状况:
--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
总结下来:
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