触发器可以做很多事情,但也会带来很多问题。使用它的技巧在于在适当的时候使用,而不要在不适当的时候使用它们。
触发器的一些常见用途如下:
- 弹性参照完整性:实现很多DRI不能实现的操作(例如,跨数据库或服务器的参照完整性以及很多复杂的关系类型)。
- 创建神级跟踪:这意味写出的记录不仅跟踪大多数当前的数据,还包括对每个记录进行实际修改的历史数据。随着SQL Server2008中的更改数据跟踪功能的出现,创建审计跟踪不再那么流行,但以前使用的就是触发器。
- 执行与CHECK约束类似的功能,但是跨表,跨数据库甚至是跨服务器使用。
- 用自己的语句代替用户的操作语句。
一、触发器的概念
触发器是一种特殊类型的存储过程,对特定事件作出相应。触发器有两种类型:数据定义语言(DDL)触发器和数据操纵语言(DML)触发器。
DDL触发器在用户以某些方式(CREATE、ALTER、DROP或相似的语句)对数据库结构进行修改时激活作出响应。一般来说,只会在对数据库结构的改变或历史进行极为严格的审计时才会用到DDL触发器。
DML触发器是一些附加在特定表或视图上的代码片段。与需要显式调用代码的存储过程不同,只要有附加触发器的时间在表中发生,触发器中的代码就会自动运行。实际上也不能显式地调用触发器-唯一的做法是在指定的表中执行所需的操作。
除了不能够显式地调用触发器,还可在存储过程中发现另外两个触发器所没有的内容:参数和返回码。
可将触发器附加到什么事件呢?因为在SQL中可以使用3类动作查询,所以就有3种类型的触发器,另外加上混合搭配这些时间并对时间定时激活的混合触发器类型。
- INSERT触发器
- DELETE触发器
- UPDATE触发器
- 以后任意类型的混合
注意:
值得注意的是,有时即使执行的动作是前面这些类型中的一种,触发器也不会激活。问题在于进行的操作是否在记录的活动中。例如,DELETE语句是一个正常的记录活动,它会激活任何删除触发器,而TRUNCATE TABLE也有删除行的作用,但只是把表使用的空间释放而已-没有记录单个行删除操作,所以没有激活任何触发器。批量操作默认情况下不激活触发器,需要显式告知批量操作激活触发器。
创建触发器的语法:
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的意义是一样的。
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过程中的某些固有内容使其显得不如触发器更为可取。
想要使用触发器而非CHECK约束的例子包括:
- 业务规则需要引用单个表中的数据。
- 业务规则需要检查更新的变化。
- 需要一个定制的错误消息。
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
现在我们来添加一个产品:
INSERT INTO [Order] VALUES(3,2,GETDATE())
显示消息如下:
我们看到,当Product的库存不足时,将不允许添加订单。
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
添加超过10条的时候
UPDATE Product SET ProductNumber = ProductNumber + 11 WHERE ProductId = 1
显示结果如下:
添加少于10条的时候
UPDATE Product SET ProductNumber = ProductNumber + 1 WHERE ProductId = 1
显示结果如下:
3、将触发器用于自定义错误消息
当想要对传给用户或客户端应用程序的错误消息或错误号进行控制时,使用触发器是很方便的。
例如,如果使用CHECK约束,只能得到标准的547错误,并且没有详尽的解释。通常,对于想知道具体错误的用户来说,这是无用的信息-缺失,客户端应用程序经常因为没有足够的信息而不能代表用户做出只能和有帮助的响应。
简而言之,当已经具备了数据完整性,但是没有足够的信息进行处理的时候,可以创建触发器。
注意:
尽管传递自定义错误代码很有用,但SQL Server中对自定义错误消息的需求还是相对较少。为什么不传递自定义错误消息呢?原因在于某些用户认为自定义错误消息之上有一个应用程序层,并且可能需要更多有关错误的上下文信息,因此特定于SQL Server的文本就无法充分发挥作用。而这时如果使用特定的错误代码,对于应用程序则有很大帮助,有助于确定确切发生的事件以及应用正确的客户端错误处理代码。
三、触发器的常见用途
1、触发器可以嵌套
嵌套的触发器是指那些不是由发出语句直接激活的,而是由另一个触发器发出的语句激活的触发器。
这实际上会引起一连串的事件,一个触发器激活另一个触发器,而另一个触发器又激活其他触发器。
触发器可以激活的深度取决于以下几个因素:
- 嵌套的触发器是否已在系统中打开(这是系统级的而不是数据库级的选项;可以使用sp_configure来设置,默认为打开的)。
- 是否有嵌套的深度不超过32层。
- 触发器是否已经被激活。触发器默认为每个触发器事务只能被激活一次。一旦被激活,则触发器会忽略其他任何调用,将这些调用作为相同触发器动作的一部分。一旦执行一条全新的语句,处理过程就会重新开始。
注意,如果在嵌套链中的任何地方进行了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>
除了模式问题之外,删除触发器就和删除其他对象一样简单了。