SQL Server 触发器

from:http://www.cnblogs.com/kissdodog/p/3173421.html html

触发器能够作不少事情,但也会带来不少问题。使用它的技巧在于在适当的时候使用,而不要在不适当的时候使用它们。sql

  触发器的一些常见用途以下:数据库

  • 弹性参照完整性:实现不少DRI不能实现的操做(例如,跨数据库或服务器的参照完整性以及不少复杂的关系类型)。
  • 建立神级跟踪:这意味写出的记录不只跟踪大多数当前的数据,还包括对每一个记录进行实际修改的历史数据。随着SQL Server2008中的更改数据跟踪功能的出现,建立审计跟踪再也不那么流行,但之前使用的就是触发器。
  • 执行与CHECK约束相似的功能,可是跨表,跨数据库甚至是跨服务器使用。
  • 用本身的语句代替用户的操做语句。

1、触发器的概念

  触发器是一种特殊类型的存储过程,对特定事件做出相应。触发器有两种类型:数据定义语言(DDL)触发器和数据操纵语言(DML)触发器。服务器

  DDL触发器在用户以某些方式(CREATE、ALTER、DROP或类似的语句)对数据库结构进行修改时激活做出响应。通常来讲,只会在对数据库结构的改变或历史进行极为严格的审计时才会用到DDL触发器。并发

  DML触发器是一些附加在特定表或视图上的代码片断。与须要显式调用代码的存储过程不一样,只要有附加触发器的时间在表中发生,触发器中的代码就会自动运行。实际上也不能显式地调用触发器-惟一的作法是在指定的表中执行所需的操做。函数

  除了不可以显式地调用触发器,还可在存储过程当中发现另外两个触发器所没有的内容:参数和返回码。性能

  可将触发器附加到什么事件呢?由于在SQL中可使用3类动做查询,因此就有3种类型的触发器,另外加上混合搭配这些时间并对时间定时激活的混合触发器类型。测试

  • INSERT触发器
  • DELETE触发器
  • UPDATE触发器
  • 之后任意类型的混合

  注意:加密

  值得注意的是,有时即便执行的动做是前面这些类型中的一种,触发器也不会激活。问题在于进行的操做是否在记录的活动中。例如,DELETE语句是一个正常的记录活动,它会激活任何删除触发器,而TRUNCATE TABLE也有删除行的做用,但只是把表使用的空间释放而已-没有记录单个行删除操做,因此没有激活任何触发器。批量操做默认状况下不激活触发器,须要显式告知批量操做激活触发器。spa

  建立触发器的语法:

  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子句用来之处触发器将要附加的表,以及在什么时候何种状况下激活这个触发器。

  一、ON子句

  这部分只是对建立触发器所针对的对象进行命名。记住,若是触发器的类型是AFTER触发器(使用FOR或AFTER来声明触发器),那么ON子句的目标就必须是一个表-AFTER触发器不支持视图。

  二、WITH ENCRYPTION选项

  加密触发器。若是添加了这个选项,则能够确保没有人可以查看你的代码(甚至是你本身)。和视图与存储过程同样,使用WITH ENCRYPTION选项须要记住的是,每次在触发器上使用ALTER语句时都必须从新应用该选项,若是使用ALTER STATEMENT语句但不包含WITH ENCRYPTION选项,那么触发器就再也不被加密。

  三、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

  一、INSERT触发器

  当有人向表中插入新的一行时,被标记为FOR INSERT的触发器的代码就会执行。对于插入的每一行来讲,SQL Server会建立一个新行的副本并把该副本插入到一个特殊的表中,该表只在触发器的做用域内存在,该表被称为Inserted表。特别须要注意的是,Inserted表只在触发器激活时存在。在触发器开启以前或完成以后,都要认为该表示不存在的。

  二、DELETE触发器

  它和INSERT触发器的工做方式相同,只是Inserted表示空的(毕竟是进行删除而非插入,因此对于Inserted表示没有记录)。相反,每一个被删除的记录的副本将会插入到另外一个表中,该表称为Deleted表,和Inserted表相似,该表只存在于触发器激活的时间内。

  三、UPDATE触发器

  除了有一点改变之外,UPDATE触发器和前面的触发器是很相似的。对表中现有的记录进行修改时,都会激活被声明FOR UPDATE的触发器的代码。惟一的改变是没有UPDATE表。SQL Server认为每一行好像删除了现有记录,并插入了全新的记录。声明为FOR UPDATE的触发器并非只包含一个表,而是两个特殊的表,称为Inserted表和Deleted表。固然,这两个表的行数是彻底相同。

  四、WITH APPEND选项

  WITH APPEND选项并不经常使用,老实讲,用到它的可能性很小;WITH APPEND选项只能应用于6.5兼容模式中。

  若是已经声明了一个称为trgCheck的触发器在更新和插入时强制执行数据完整性,那么就不能建立另外一个触发器来进行级联更新。一旦建立了更新(或插入、删除)触发器,那么就不能建立另外一个同一动做类型的触发器。为解决这个问题,WITH APPEND子句显式地告诉SQL Server,即便在表上已经有了这种类型的触发器,还能够添加一个新的触发器。当有合适的触发动做(INSERT、UPDATE、DELETE)发生时,会同时激活两个触发器。

  五、NOT FOR REPLICATION选项

  若是添加了该选项,会稍微地改变关于什么时候激活触发器的规则。在适当的位置使用这个选项,不管与复制相关的任务什么时候修改表,都不会激活触发器。一般,当修改了原始表,而且不会再进行修改的时候会激活触发器(进行内务处理或级联等操做)。

  六、AS子句

  和在存储过程当中的使用彻底相同,这正是触发器的实质所在。AS关键字告诉SQL Server,代码将要启动。

2、使用触发器实施数据完整性规则

  虽然触发器不会成为首要的选择,可是触发器也一样能够执行和CHECK约束甚至是DEFAULT约束同样的功能。使用触发器仍是CHECK约束?答案是:看状况而定。若是CHECK约束能够完成,那么可能CHECK约束是更受青睐的选择。可是,有时会出现CHECK约束不能完成任务的状况,或是CHECK过程当中的某些固有内容使其显得不如触发器更为可取。

  想要使用触发器而非CHECK约束的例子包括:

  • 业务规则须要引用单个表中的数据。
  • 业务规则须要检查更新的变化。
  • 须要一个定制的错误消息。

  一、处理来自于其余表的需求

  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的库存不足时,将不容许添加订单。

  二、使用触发器检查更新的变化

  有时,你可能并不关心过去的值和如今的值,只是想知道变化的量。虽然没有任何列或表给出这些信息,可是能够在触发器中使用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

  显示结果以下:

  

  三、将触发器用于自定义错误消息

  当想要对传给用户或客户端应用程序的错误消息或错误号进行控制时,使用触发器是很方便的。

  例如,若是使用CHECK约束,只能获得标准的547错误,而且没有详尽的解释。一般,对于想知道具体错误的用户来讲,这是无用的信息-缺失,客户端应用程序常常由于没有足够的信息而不能表明用户作出只能和有帮助的响应。

  简而言之,当已经具有了数据完整性,可是没有足够的信息进行处理的时候,能够建立触发器。

  注意:

  尽管传递自定义错误代码颇有用,但SQL Server中对自定义错误消息的需求仍是相对较少。为何不传递自定义错误消息呢?缘由在于某些用户认为自定义错误消息之上有一个应用程序层,而且可能须要更多有关错误的上下文信息,所以特定于SQL Server的文本就没法充分发挥做用。而这时若是使用特定的错误代码,对于应用程序则有很大帮助,有助于肯定确切发生的事件以及应用正确的客户端错误处理代码。

3、触发器的常见用途

   一、触发器能够嵌套

  嵌套的触发器是指那些不是由发出语句直接激活的,而是由另外一个触发器发出的语句激活的触发器。

  这实际上会引发一连串的事件,一个触发器激活另外一个触发器,而另外一个触发器又激活其余触发器。

  触发器能够激活的深度取决于如下几个因素:

  •   嵌套的触发器是否已在系统中打开(这是系统级的而不是数据库级的选项;可使用sp_configure来设置,默认为打开的)。
  •   是否有嵌套的深度不超过32层。
  •   触发器是否已经被激活。触发器默认为每一个触发器事务只能被激活一次。一旦被激活,则触发器会忽略其余任何调用,将这些调用做为相同触发器动做的一部分。一旦执行一条全新的语句,处理过程就会从新开始。

  注意,若是在嵌套链中的任何地方进行了ROLLBACK操做,那么整条链都会回滚。换句话说,整个触发器链就像一个事务同样。

   二、触发器能够递归

  什么是递归触发器?若是某触发器所作的事情最终激活了自身,那么该触发器就是递归的。能够直接触发(经过设置了触发器的表进行动做查询来完成),也能够间接触发(经过嵌套过程)。

   递归触发器比较少见,默认状况下,递归触发器是关闭的。递归是数据库级的选项,可使用sp_dboption系统存储过程来设置。

  递归触发器的风险在于可能会陷入某种非预设的循环之中。这样便须要确保在必要的时候能够经过递归检查的形式来中止这一过程。

  三、触发器不能防止体系结构的修改

  触发器有助于更容易地修改体系结构。事实上,一般在开发周期的早期使用触发器实施参照完整性,而在后期,也就是要进入生产环境时将其改成DRI。

  四、能够在不删除的状况下关闭触发器

  有时,像CHECK约束同样,你想要关闭完整性功能以便于执行一些违反约束可是有效的动做(最多见的就是导入数据)。

  可使用ALTER语句来关闭触发器,语法以下:

  ALTER TABLE <table name>
    <ENABLE|DISABLE> TRIGGER <ALL|<trigger name>>

  若是关闭触发器是为了导入数据,那么建议踢出全部用户并进入单用户模式。dbo-only模式,或同时进入两种模式。这样一来,当关闭触发器时,就能确保万无一失。

  五、触发器的激活顺序

  对于任何给定的表(只有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-也就是说,没有特殊激活顺序的触发器的数量是没有限制的。

  

  为何要控制激活顺序

  一、出于逻辑缘由而控制激活顺序

  为何要在激活一个触发器以前去激活另外一个触发器。最多见的理由是第一个触发器是后面触发器的基础或前面的触发器使后面的触发器有效。

  二、处于性能缘由而控制激活顺序

  在性能方面,FIRST触发器是惟一块儿关键做用的触发器,若是有多个触发器,可是其中只有一个触发器可能会产生回滚,那么就须要考虑将这个触发器标记为FIRST触发器,这能令外回滚的操做更少。

4、性能考虑

  一、触发器的被动型

  这里的意思是指触发器发生在事务以后。当激活触发器时,整个查询已经运行而且事务也已经被记录到日志中(但未提交,只是记录到激活触发器的语句点)。这意味着若是触发器须要回滚,那么必须撤销已经作的全部工做。这和约束是不一样,约束是主动的,约束是发生在语句真正执行前。这意味着约束会检测可能失败的操做,而且在进程的前期就予以阻止。因此约束一般运行得快一些-在更为复杂的查询中速度更快。注意,只有在发生回滚时,约束明显更快。

   若是正在处理少许回滚,并且受影响的语句的复杂性较低,执行之间较短,那么触发器和约束之间没有太大的区别。可是在没法预知回滚的数量的时候,坚持使用约束的效率更好。

  二、触发器与激活的进程之间不存在并发问题

  若是激活语句不是显示事务的一部分,那么该语句仍然是其自身的但语句事务的一部分。不管何种状况,触发器内部发出的ROLLBACK TRAN仍然会回滚整个事务。

  这种同属一个事务的另外一个结果是触发器继承了他们所属事务上已打开的锁。这意味着不须要作任何特殊的处理来避免碰到事务中其余语句建立的锁。在事务的做用域内能够自由访问,而且能够发现数据库基于事务中先前的语句所做的修改。

  三、使用IF UPDATE()和COLUMNS_UPDATE()

  在UPDATE触发器中,能够经过检查感兴趣的列是否已被修改来限制在触发器中执行的代码总量。为了实现这一点,可使用UPDATE()或COLUMN_UPDATE()函数。

  一、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

  显示结果以下:

  

  二、COLUMNS_UPDATE()函数

  这个函数和UPDATE()的运行方式不一样,但目的相同。COLUMNS_UPDATE()函数能够一次检查多列。为了实现这一点,该函数使用了位掩码,位掩码将varbinary数据的一个或多个字节中的单个位与表中的单个列相关联。

  

  对于上图的状况,数据的单个字节说明了第2,第3,以及第6列已经更新,而其余列没有更新。

  对于超过8列的状况,SQL Server就会在右边添加另外一个字节而且继续计数。

  

  对于上图,此次是跟心了第2,第9以及第14列。

  这些信息怎么使用呢?

  •   |  表示 或
  •   &   表示 与
  •   ^   表示 异或

  示例:

  COLUMN_UPDATE()>0  检查是否有列被更新。

  COLUMN_UPDATE()^21=0  检查是否更新了全部指定列(一、三、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)
复制代码

  五、尽可能别在触发器中回滚

  若是在触发器中使用不少的ROLLBACK TRAN语句,那么请确保在执行激活触发器的语句前预先进行错误检查。SQL Server在这种状况下,是被动的,但你能够主动。时间检查错误,而不是等待回滚。

  由于回滚的代价是昂贵的。

5、删除触发器

  删除触发器和普通删除操做略有不一样,和表同样,其问题在于触发器的名称被限定在模式级别。这意味着一个触发器能够有两个名称相同的对象,只要方式触发器的对象与触发器另外一个同名的对象位于不一样的模式中。重申一次,触发器是以其所处的模式命名的,而不是以触发器所关联的对象命名。

  删除触发器的语法以下:

  DROP TRIGGER [<schema>.]<trigger name>

  除了模式问题以外,删除触发器就和删除其余对象同样简单了。

相关文章
相关标签/搜索