触发器分类
SQL Server提供三类触发器:
- DML触发器:在数据库中发生数据操作(Insert、Update 或Delete)事件时自动执行。
- DDL触发器:在服务器或数据库中发生数据定义(如Create、Alter、Drop)事件时自动执行。
- 登录触发器:在与SQL Server实例建立用户会话时自动执行,主要用来审核和控制服务器会话。
与Oracle对比,SQL Server的触发器有以下特点:
- SQL Server不支持Oracle的Before触发器功能。
虽然没有Before触发器功能,但是SQL Server可以利用Instead Of重写关键字,然后在触发器中加入想要在DML操作之前的事务。
- SQL Server不支持Oracle的row-level触发器功能,而只支持类似Oracle的statement-level形式的触发器,即DML语句只会激活一次触发器的执行,而不管其影响了多少条记录。
Oracle的row-level触发器:创建触发器时附加for each row关键字,对触发事件所影响的每条记录都执行一次触发器。如果一个update语句修改了10条记录,则row-level触发器会执行10次。
虽然SQL Server不支持row-level,但是我们可以通过INSERTED表和DELETED表的功能。比如Oracle触发器中的这条语句:
for each row when(abs(new.sal>old.sal)>2000)
在SQL Server中可以写成这样:
if(select max(abs(i.sal-d.sal)) from inserted i,deleted d
where i.empno=d.empno)>2000
- SQL Server的DDL触发器只支持After形式。
SQL Server的DDL触发器只有在完成相应的DDL语句之后才会被触发,因此DDL触发器不能使用INSTEAD OF选项。
- SQL Server的DDL触发器除与Oracle的DDL触发器功能类似外,还包含了Oracle的系统触发器的部分功能
DML触发器
了解DML触发器
当数据库中发生数据操作语言 (DML) 事件时将调用 DML 触发器。DML 事件包括在指定表或视图中修改数据的 INSERT 语句、UPDATE 语句或 DELETE 语句。DML 触发器可以查询其他表,还可以包含复杂的 Transact-SQL 语句。将触发器和触发它的语句作为可在触发器内回滚的单个事务对待。如果检测到错误(例如,磁盘空间不足),则整个事务即自动回滚。
DML 触发器在以下方面非常有用:
- DML 触发器可通过数据库中的相关表实现级联更改。不过,通过级联引用完整性约束可以更有效地进行这些更改。
- DML 触发器可以防止恶意或错误的 INSERT、UPDATE 以及 DELETE 操作,并强制执行比 CHECK 约束定义的限制更为复杂的其他限制。
与 CHECK 约束不同,DML 触发器可以引用其他表中的列。例如,触发器可以使用另一个表中的 SELECT 比较插入或更新的数据,以及执行其他操作,如修改数据或显示用户定义错误信息。
DML 触发器可以评估数据修改前后表的状态,并根据该差异采取措施。 - 一个表中的多个同类 DML 触发器(INSERT、UPDATE 或 DELETE)允许采取多个不同的操作来响应同一个修改语句。
DML 触发器的类型
(1)AFTER 触发器
在执行了 INSERT、UPDATE 或 DELETE 语句操作之后执行 AFTER 触发器。指定 AFTER 与指定 FOR 相同,它是 Microsoft SQL Server 早期版本中唯一可用的选项。AFTER 触发器只能在表上指定。
AFTER触发器仅适用于表,每个触发操作(UPDATE、DELETE 和 INSERT)可以包含多个触发器。
由上图可见AFTER触发器执行晚于约束处理、声明性引用操作、创建插入的和删除的表。
(2)INSTEAD OF 触发器
执行 INSTEAD OF 触发器代替通常的触发动作。还可为带有一个或多个基表的视图定义 INSTEAD OF 触发器,而这些触发器能够扩展视图可支持的更新类型。
INSTEAD OF触发器既可以用于表也可以用于视图,但是每个触发操作(UPDATE、DELETE 和 INSERT)只能包含一个触发器。
由上图可见INSTEAD OF触发器早于约束处理,替代触发操作,晚于创建插入的和删除的表。
(3)CLR 触发器
CLR 触发器可以是 AFTER 触发器或 INSTEAD OF 触发器。CLR 触发器还可以是 DDL 触发器。CLR 触发器将执行在托管代码(在 .NET Framework 中创建并在 SQL Server 中上载的程序集的成员)中编写的方法,而不用执行 Transact-SQL 存储过程。
创建DML触发器
当DML触发器执行的时候,会产生两个只读的临时表:inserted表和deleted表。这两个表的结构和触发器所在的表的结构相同。当触发器执行完成之后,这两个表就会被自动删除。
当对触发器表执行INSERT操作时,新插入的行将同时被添加到inserted表中,inserted表中的行是触发器表中新添加行的副本。
当对触发器表执行DELETE操作时,行将从触发器表中删除,同时被存入deleted表中。deleted表和触发器表没有相同的行。
当对触发器表执行UPDATE操作时,先从触发器表中删除旧行,然后再插入新行。其中被删除的旧行被插入到deleted表中,插入的新行的副本被插入到inserted表中。
创建DML触发器语法如下:
IF EXISTS(select * from sys.sysobjects where name='<触发器名>')
DROP TIGGER <触发器名>
GO
CREATE TIGGER <触发器名>
ON <表名|视图名>
[WITH ENCRYPTION]
FOR|AFTER|INSTEAD OF
[INSERT][,][UPDATE][,][DELETE]
AS
<T-SQL语句或语句块>
- WITH ENCRYPTION:对触发器进行加密处理
- FOR|AFTER:FOR和AFTER意义相同,指定触发器中在相应的DML操作成功执行后才触发。
- INSTEAD OF:指定执行DML触发器用于“代替”引发触发器执行的DML语句。
- [INSERT][,][UPDATE][,][DELETE]:指定能够激活触发器的操作,必须至少指定一个操作
使用触发器小Tips
①触发器本身就是一个事务,所以在触发器里面可以对修改数据进行一些特殊的检查。如果不满足可以利用事务回滚,撤销操作(区别于过程和函数,过程和函数不能在语句块里面进行事务的commit、rollback等操作)。
②SQL Server不支持Oracle创建触发器时的update of column的语法形式,也就是不能判断指定哪一列被Update操作才触发触发器。但是在SQL Server中我们可以通过update(column_name)的形式来判断列是否被修改过,详见示例(2)。
【示例】
(1)级联修改数据
--update更新类型触发器
if (object_id('tgr_classes_update', 'TR') is not null)
drop trigger tgr_classes_update
go
create trigger tgr_classes_update
on classes
for update
as
declare @oldName varchar(20), @newName varchar(20);
--更新前的数据
select @oldName = name from deleted;
if (exists (select * from student where name like '%'+ @oldName + '%'))
begin
--更新后的数据
select @newName = name from inserted;
update student set name = replace(name, @oldName, @newName)
where name like '%'+ @oldName + '%';
print '级联修改数据成功!';
end
else
print '无需修改student表!';
go
(2)审计对emp表的sal列的update操作
create tigger tri_emp_sal_audit
on emp
after update
as
if update(sal)
insert into emp_sal_audit
select i.empno,d.sal,i.sal,user,getdate()
from inserted i,deleted d
where i.empno=d.empno
go
设计INSTEAD OF触发器
INSTEAD OF 触发器的主要优点是可以使不能更新的视图支持更新。基于多个基表的视图必须使用 INSTEAD OF 触发器来支持引用多个表中数据的插入、更新和删除操作。INSTEAD OF 触发器的另一个优点是使您得以编写这样的逻辑代码:在允许批处理的其他部分成功的同时拒绝批处理中的某些部分。
INSTEAD OF 触发器可以进行以下操作:
- 忽略批处理中的某些部分。
- 不处理批处理中的某些部分并记录有问题的行。
- 如果遇到错误情况则采取备用操作。
适用INSTEAD OF触发器注意事项:
- 对于含有使用 DELETE 或 UPDATE 级联操作定义的外键的表,不能定义 INSTEAD OF DELETE 和 INSTEAD OF UPDATE 触发器。
- 因为INSTEAD OF触发器的内容会代替DELETE、UPDATE、INSERT触发操作,所以在INSTEAD OF触发器内要通过inserted表和deleted表重写DML操作。
【示例】
(1)创建触发器禁止用户在每天晚上18:00:00到23:59:59之间以及凌晨00:00:00到06:59:59之间对emp表执行update操作。
create tigger tri_deny_emp_update
on emp
instead of update
as
if(datepart(hour,getdate())>=18 and datepart(hour,getdate())<=23)
or(datepart(hour,getdate())>=0 and datepart(hour,getdate())<=6)
print 'Update not allowed now.'
else
begin
delete from emp where empno in (select empno from deleted)
insert into emp select * from inserted
end
go
(2)在下列 Transact-SQL 语句序列中,INSTEAD OF 触发器更新视图中的两个基表。另外,显示以下处理错误的方法:
- 忽略对 Person 表的重复插入,并且插入的信息将记录在 PersonDuplicates 表中。
- 将对 EmployeeTable 的重复插入转变为 UPDATE 语句,该语句将当前信息检索至 EmployeeTable,而不会产生重复键冲突。
T-SQL 语句创建两个基表、一个视图、一个记录错误表和视图上的 INSTEAD OF 触发器。以下表将个人数据和业务数据分开并且是视图的基表。
-- 两个基表
CREATE TABLE Person
(
SSN char(11) PRIMARY KEY,
Name nvarchar(100),
Address nvarchar(100),
Birthdate datetime
)
CREATE TABLE EmployeeTable
(
EmployeeID int PRIMARY KEY,
SSN char(11) UNIQUE,
Department nvarchar(10),
Salary money,
CONSTRAINT FKEmpPer FOREIGN KEY (SSN)
REFERENCES Person (SSN)
)
-- 可记录对插入具有重复的社会保障号的行的尝试。
-- PersonDuplicates 表记录插入的值、尝试插入的用户的姓名和插入的时间。
CREATE TABLE PersonDuplicates
(
SSN char(11),
Name nvarchar(100),
Address nvarchar(100),
Birthdate datetime,
InsertSNAME nchar(100),
WhenInserted datetime
)
-- 一个视图
CREATE VIEW Employee AS
SELECT P.SSN as SSN, Name, Address,
Birthdate, EmployeeID, Department, Salary
FROM Person P, EmployeeTable E
WHERE P.SSN = E.SSN
INSTEAD OF触发器:
CREATE TRIGGER IO_Trig_INS_Employee ON Employee
INSTEAD OF INSERT
AS
BEGIN
SET NOCOUNT ON
-- Check for duplicate Person. If there is no duplicate, do an insert.
IF (NOT EXISTS (SELECT P.SSN
FROM Person P, inserted I
WHERE P.SSN = I.SSN))
INSERT INTO Person
SELECT SSN,Name,Address,Birthdate FROM inserted
ELSE
-- Log an attempt to insert duplicate Person row in PersonDuplicates table.
INSERT INTO PersonDuplicates
SELECT SSN,Name,Address,Birthdate,SUSER_SNAME(),GETDATE() FROM inserted
-- Check for duplicate Employee. If no there is duplicate, do an INSERT.
IF (NOT EXISTS (SELECT E.SSN
FROM EmployeeTable E, inserted
WHERE E.SSN = inserted.SSN))
INSERT INTO EmployeeTable
SELECT EmployeeID,SSN, Department, Salary FROM inserted
ELSE
--If there is a duplicate, change to UPDATE so that there will not
--be a duplicate key violation error.
UPDATE EmployeeTable
SET EmployeeID = I.EmployeeID,
Department = I.Department,
Salary = I.Salary
FROM EmployeeTable E, inserted I WHERE E.SSN = I.SSN
END
DDL触发器
了解DDL触发器
像常规触发器一样,DDL 触发器将激发存储过程以响应事件。但与 DML 触发器不同的是,它们不会为响应针对表或视图的 UPDATE、INSERT 或 DELETE 语句而激发。相反,它们将为了响应各种数据定义语言 (DDL) 事件而激发。这些事件主要与以关键字 CREATE、ALTER 和 DROP 开头的 Transact-SQL 语句对应。执行 DDL 式操作的系统存储过程也可以激发 DDL 触发器。
测试您的 DDL 触发器以确定它们是否响应运行的系统存储过程。例如,CREATE TYPE 语句和 sp_addtype 存储过程都将激发针对 CREATE_TYPE 事件创建的 DDL 触发器。
DDL 触发器可用于管理任务,例如审核和控制数据库操作。如果要执行以下操作,请使用 DDL 触发器:
- 要防止对数据库架构进行某些更改。
- 希望数据库中发生某种情况以响应数据库架构中的更改。
- 要记录数据库架构中的更改或事件。
DDL触发器的作用域
在响应当前数据库或服务器上处理的 Transact-SQL 事件时,可以触发 DDL 触发器。触发器的作用域取决于事件。例如,每当数据库中或服务器实例上发生 CREATE_TABLE 事件时,都会激发为响应 CREATE_TABLE 事件创建的 DDL 触发器。仅当服务器上发生 CREATE_LOGIN 事件时,才能激发为响应CREATE_LOGIN 事件创建的 DDL 触发器。
DDL事件以及DDL事件组
请参阅:https://msdn.microsoft.com/zh-cn/library/bb522542(v=sql.105).aspx
使用 EVENTDATA 函数
使用 EVENTDATA 函数,可以捕获有关激发 DDL 触发器的事件的信息。此函数返回 xml 值。XML 架构包括下列信息:
- 事件时间。
- 在执行触发器时,连接的系统进程 ID (SPID)。
- 激发触发器的事件类型。
根据事件类型,该架构还包括其他信息,例如事件在其中发生的数据库、发生事件的相关对象以及事件的 Transact-SQL 语句。
只有直接在 DDL 或登录触发器内部引用 EVENTDATA 时,EVENTDATA 才会返回数据。如果 EVENTDATA 由其他例程调用(即使这些例程由 DDL 或登录触发器进行调用),将返回 NULL。在隐式或显式调用 EVENTDATA 的事务提交或回滚之后,EVENTDATA 所返回的数据将无效。
由于EVENTDATE返回的是xml,所以要对xml进行解析。所幸,SQL Server提供了XQuery。XQuery 是一种可以查询结构化或半结构化 XML 数据的语言,基于XPath,并支持更好的迭代、更好的排序结果以及构造必需的 XML 的功能。详情请参阅:https://msdn.microsoft.com/zh-cn/library/ms190798(v=sql.105).aspx
如果需要返回事件数据,我们建议使用 XQuery value() 方法而不是 query() 方法。query() 方法可在输出中返回 XML 和以“and”符转义的回车符和换行符 (CR/LF) 实例,而 value() 方法无法在输出中呈现 CR/LF 实例。
下面提供一些常用的解析:
-- 受影响的时间
@data.value('(/EVENT_INSTANCE/PostTime)[1]', 'sysname')
-- 受影响的服务器名
@data.value('(/EVENT_INSTANCE/ServerName)[1]', 'sysname')
-- 受影响的登陆名
@data.value('(/EVENT_INSTANCE/LoginName)[1]', 'sysname')
-- 受影响的用户名
@data.value('(/EVENT_INSTANCE/UserName)[1]', 'sysname')
-- 受影响的数据库名
@data.value('(/EVENT_INSTANCE/DatabaseName)[1]', 'sysname')
-- 受影响的架构名
@data.value('(/EVENT_INSTANCE/SchemaName)[1]', 'sysname')
-- 受影响的表名
@data.value('(/EVENT_INSTANCE/ObjectName)[1]', 'sysname')
-- 受影响的sql语句
@data.value('(/EVENT_INSTANCE/TSQLCommand)[1]', 'nvarchar(max)')
-- 事件类型
@data.value('(/EVENT_INSTANCE/EventType)[1]', 'sysname')
创建DDL触发器
CREATE TIGGER <触发器名>
ON ALL SERVER|DATABASE
[WITH ENCRYPTION]
FOR|AFTER <事件类型或事件组>[,...,n]
[INSERT][,][UPDATE][,][DELETE]
AS
<T-SQL语句或语句块>
- ALL SERVER:指定DDL触发器的作用域为当前服务器。如果指定了此参数,则只要当前服务器中的任何位置上出现命令中指定的事件类型或事件组,就会激发该触发器。创建的触发器可在“SQL Server实例→服务器对象→触发器”窗格中显示。
- DATABASE:指定DDL触发器的作用域为当前数据库。如果指定了此参数,则只要当前数据库中出现命令中指定的事件类型或事件组,就会激发该触发器。创建的触发器可在“SQL Server实例→数据库→(数据库名称)→可编程性→数据库触发器”窗格中显示。
- 事件类型:将激活DDL触发器的T-SQL事件的名称。
- 事件组:预定义的T-SQL语句事件分组名称。执行任何属于事件组的T-SQL语句事件之后,都将激发DDL触发器。
【示例】
A、运用具有数据库范围的 DDL 触发器
下面的示例使用 DDL 触发器来防止从数据库中删除任何同义词。
IF EXISTS (SELECT * FROM sys.triggers
WHERE parent_class = 0 AND name = 'safety')
DROP TRIGGER safety
ON DATABASE;
GO
CREATE TRIGGER safety
ON DATABASE
FOR DROP_SYNONYM
AS
RAISERROR ('You must disable Trigger "safety" to drop synonyms!',10, 1)
ROLLBACK
GO
B、运用具有服务器范围的 DDL 触发器
在以下示例中,如果当前服务器实例上出现任何 CREATE DATABASE 事件,则使用 DDL 触发器输出一条消息,并使用 EVENTDATA 函数检索对应 Transact-SQL 语句的文本。
IF EXISTS (SELECT * FROM sys.server_triggers
WHERE name = 'ddl_trig_database')
DROP TRIGGER ddl_trig_database
ON ALL SERVER;
GO
CREATE TRIGGER ddl_trig_database
ON ALL SERVER
FOR CREATE_DATABASE
AS
PRINT 'Database Created.'
SELECT EVENTDATA().value('(/EVENT_INSTANCE/TSQLCommand/CommandText)[1]','nvarchar(max)')
GO
C、创建事件数据在 DDL 触发器中的日志表
CREATE TABLE ddl_log (
PostTime datetime,
DB_User nvarchar(100),
Event nvarchar(100),
TSQL nvarchar(2000))
GO
CREATE TRIGGER log
ON DATABASE
FOR DDL_DATABASE_LEVEL_EVENTS
AS
DECLARE @data XML;
SET @data = EVENTDATA();
INSERT ddl_log (PostTime, DB_User, Event, TSQL)
VALUES
(GETDATE(),
CONVERT(nvarchar(100), CURRENT_USER),
@data.value('(/EVENT_INSTANCE/EventType)[1]', 'nvarchar(100)'),
@data.value('(/EVENT_INSTANCE/TSQLCommand)[1]', 'nvarchar(2000)') ) ;
GO
登录触发器
登录触发器将为响应 LOGON 事件而执行存储过程。与 SQL Server 实例建立用户会话时将引发此事件。登录触发器将在登录的身份验证阶段完成之后且用户会话实际建立之前激发。如果身份验证失败,将不激发登录触发器。
在登录触发器中不支持分布式事务。在激发包含分布式事务的登录触发器时,将返回错误 3969。
【示例】
下面的登录触发器示例拒绝了作为 login_test 登录名的成员登录 SQL Server 的尝试(如果在此登录名下已运行三个用户会话)。
USE master;
GO
CREATE LOGIN login_test WITH PASSWORD = '3KHJ6dhx(0xVYsdf' MUST_CHANGE,
CHECK_EXPIRATION = ON;
GO
GRANT VIEW SERVER STATE TO login_test;
GO
CREATE TRIGGER connection_limit_trigger
ON ALL SERVER WITH EXECUTE AS 'login_test'
FOR LOGON
AS
BEGIN
IF ORIGINAL_LOGIN()= 'login_test' AND
(SELECT COUNT(*) FROM sys.dm_exec_sessions
WHERE is_user_process = 1 AND
original_login_name = 'login_test') > 3
ROLLBACK;
END;
管理触发器
①查看触发器信息
因为触发器是特殊的存储过程,所以查看触发器也是使用系统存储过程实现,如:
sp_help '触发器名称'
sp_helptext '触发器名称'
sp_depends '触发器名称'|'表名'
②修改触发器
1、修改DML触发器
ALTER TIGGER <触发器名>
ON <表名|视图名>
[WITH ENCRYPTION]
FOR|AFTER|INSTEAD OF
[INSERT][,][UPDATE][,][DELETE]
AS
<T-SQL语句或语句块>
2、修改DDL触发器
ALTER TIGGER <触发器名>
ON ALL SERVER|DATABASE
[WITH ENCRYPTION]
FOR|AFTER <事件类型或事件组>[,...,n]
[INSERT][,][UPDATE][,][DELETE]
AS
<T-SQL语句或语句块>
;
③删除触发器
-- Trigger on an INSERT, UPDATE, or DELETE statement to a table or view (DML Trigger)
DROP TRIGGER [ IF EXISTS ] [schema_name.]trigger_name [ ,...n ]
-- Trigger on a CREATE, ALTER, DROP, GRANT, DENY, REVOKE or UPDATE statement (DDL Trigger)
DROP TRIGGER [ IF EXISTS ] trigger_name [ ,...n ] ON { DATABASE | ALL SERVER }
-- Trigger on a LOGON event (Logon Trigger)
DROP TRIGGER [ IF EXISTS ] trigger_name [ ,...n ] ON ALL SERVER
④禁用与启用触发器
#禁用触发器
DISABLE TIGGER <触发器名> ON 对象名|DATABASE|ALL SERVER
#启用触发器
ENABLE TIGGER <触发器名> ON 对象名|DATABASE|ALL SERVER