SQL Server从入门到项目实践(超值版)读书笔记 27
第14章 触发器的应用
🎉学习指引为了更好的管理数据文件,SQL SERVER提出了触发器的概念,触发器是确保数据完整性的一种方法。
本章介绍触发器的应用,主要内容包括触发器的分类,创建、修改、删除以及应用。
14.1 认识触发器
触发器与储存过程不同,它不需要使用EXEC语句调用,就可以被执行。
但是,在触发器中所写的语句与存储过程类似,可以说触发器是一种特殊的存储过程。触发器可以在对表进行UPDATE\INSERT和DELETE这些操作时,自动地被调用。
14.1.1 触发器的概念
触发器是一种特殊的存储过程。
与前面介绍过的存储过程不同,触发器主要是通过事件进行触发而被执行的,而存储过程可以通过存储过程名称被直接调用。
触发器是一个功能强大的工具,它使每个站点可以在有数据修改时自动强制执行器业务规则。触发器可以用于SQL SERVER约束、默认值和规则的完整性检查。
当往某一个表格中插入、修改或删除记录时,SQL SERVER就会自动执行触发器所定义的SQL语句,从而确保对数据的处理必须复合由这些SQL语句所定义的规则。在触发器中可以查询其他表格或者包括复杂的SQL语句。触发器和引起触发器执行的SQL语句被当作一次事务处理,如果这次事务未获得成功,SQL SERVER会自动返回该事务执行前的状态。
和CHECK约束相比较,触发器可以强制实现更加复杂的数据完整性,而且可以参考其他表的字段。
14.1.2 触发器的作用
触发器是一个在修改指定表的数据时执行的存储过程,不同的是执行存储过程要使用EXEC语句来调用,而触发器的执行不需要使用,通过创建触发器可以保证不同表中的逻辑相关数据的引用完整性或一致性。
它的主要作用如下:
- 触发器是自动的。当对表中的数据做了任何修改(比如手工输入或者应用程序采取的操作)之后,立即被激活。
- 触发器可以通过数据库中的相关表进行层叠更改。
- 触发器可以强制限制。这些限制比用CHECK约束所定义的更复杂。不同的是,触发器可以引用其他表中的列。
14.1.3 触发器的分类
在SQL SERVER数据库中,触发器主要分为三类,即登录触发器、DML触发器和DDL触发器。
14.1.3.1 登录触发器
登录触发器是作用在LOGIN事件的触发器,是一种AFTER类型触发器,表示在登陆后触发。使用登录触发器可以控制用户会话的创建过程以及限制用户名和会话的次数。
14.1.3.2 DML触发器
DML触发器包括对表或视图DML操作激发的触发器。DML操作包括INSERT\UPDATE\DELETE语句。DML触发器包括两种类型,一种AFTER型,一种是INSTEAD OF类型。AFTER类表示对表或视图操作完成后激活,INSTEAD OF类型表示当表或视图执行DML操作时,替代这个操作执行其他一些操作。
14.1.3.3 DDL触发器
DDL触发器时当服务器或者数据库中发生数据定义语言事件时被激活调用,使用DDL触发器可以防止对数据库架构进行某些更改或记录数据架构中的更改或事件。DDL操作包括CREATE\ALTER或DROP等,该触发器一般用于管理和记录数据库对象的结构变化。
14.2 创建触发器
创建触发器是开始使用触发器的第一步,只有创建了触发器,才可以完成后续的操作,用户可以使用SQL语句来创建触发器,也可以在SSMS中以图形界面来创建触发器。
14.2.1 创建DML触发器
DML触发器是指当数据库服务器中发生数据库操作语言事件时要执行的操作,DML事件包括对表或视图发出的UPDATE\INSERT\DELETE语句。
14.2.1.1 INSERT触发器
触发器是一种特殊类型的存储过程,因此创建触发器的语法格式与创建存储过程的语法格式相似:
CREATE TRIGGER schema_name.trigger_name
ON {table|view}
[with <dml_trigger_option> [,...n]]
{FOR | AFTER | INSTEAD OF}
{[INSERT][,][UPDATE][,][DELETE]}
[WITH APPEND]
[NOT FOR REPLICATION]
AS
{sql_statement [;] [,...n] | EXTERNAL NAME <method specifier [;]>}
<dml_teigger_option>::= [ENCRYPTION][EXECUTE AS Clause]
<method_specifier>::=assembly_name.class_name.method_name
主要参数:
- trigger_name:触发器名称,在当前数据库中必须唯一。
- table|view:在其上执行触发器的表或视图,有时称为触发器表或触发器视图。
- AFTER:指定触发器只有在触发SQL语句中指定的所有操作都已成功执行后才激发。所有的引用级联操作和约束检查也必须成功后,才能执行此触发器。如果仅指定FOR关键字,则AFTER是默认设置。注意该类型触发器仅能在表上创建,而不能在视图上定义。
- INSTEAD OF:用于规定执行的是触发器而不是执行触发SQL语句,从而用触发器替代触发语句的操作。在表或视图上,每个INSERT\UPDATE\DELETE语句最多可以定义一个INSTEAD OF触发器。然而,可以在每个具有INSTEAD OF触发器的视图上定义视图。INSTEAD OF触发器不能在WITH CHECK OPTION的可更新视图上定义。如果向指定的WITH CHECK OPTION选项的可更新视图添加INSTEAD OF触发器,系统将产生一个错误。用户必须用ALTER VIEW删除该选项后才能定义INSTEAD OF触发器。
- {[DELETE][,][INSERT][,][UPDATE]}:用于指定在表或视图上执行哪些数据修改语句时,将激活触发器的关键字。必须至少指定一个选项。在触发器定义中允许使用以任何的顺序组合这些关键字。如果指定的选项多于一个,需要用逗号分隔。
- [WITH APPEND]:指定应该再添加一个现有类型的触发器。
- AS:触发器要执行的操作。
- sql_statement:触发器的条件和操作。触发器条件指定其他准则,以确定DELETE\INSERT\UPDATE语句是否导致执行触发器操作。
当用户向表中插入新的记录行时,被标记为FOR INSERT的触发器的代码就会执行,如前所述,同时SQL SERVER会创建一个新行的副本,将副本插入到一个特殊表中。该表只在触发器的作用域内存在。下面我们就来创建当用户执行INSERT操作时触发的触发器。
例:在students表上创建一个名称为Insert_Student的触发器,在用户向students表中插入数据时触发,语句如下:
CREATE TRIGGER Insert_Student
ON students
AFTER INSERT
AS
BEGIN--先判断stu_Sum表是否存在,如果不存在,就IF OBJECT_ID(N'stu_Sum',N'U') IS NULL --创建表 stu_Sum,表中只有一个整型字段number,初始值为0CREATE TABLE stu_Sum(number INT DEFAULT 0)--定义变量@stuNumber为整型DECLARE @stuNumber INT--将students表的记录数赋值给变量SELECT @stuNumber=COUNT(*) FROM students--如果表stu_Sum中不存在记录IF NOT EXISTS (SELECT * FROM stu_Sum)--就向表中插入数据INSERT INTO stu_Sum(number) values (0)--更新学生总人数插入到表stu_Sum中UPDATE stu_Sum SET number=@stuNumber
END
GO
单击“执行”按钮,即可完成触发器的创建
触发器创建完成之后,接着向students表中插入记录,触发触发器的执行,SQL语句如下:
SELECT COUNT(*) STUDENTS表中总人数 FROM students
INSERT INTO students (id,name,sec,age) values (1004,'魏延','男',40)
SELECT COUNT(*) STUDENTS表中总人数 FROM students
SELECT number as stu_Sum表中总人数 FROM stu_Sum
单击“执行”按钮,即可完成激活触发器的执行操作
💡提示:
由触发器的触发过程可以看到,查询语句中的第2行执行了一条INSERT语句,向students表中插入一条记录,结果显示插入前后students表中总的记录数;
第4行语句查看触发器执行之后stu_Sum表中的结果,可以看到,这里成功地将students表中总的学生人数计算之后插入到stu_Sum表,实现了表的级联操作。
在某些情况下,根据数据库设计需要,可能会禁止用户对某些表的操作,可以在表上指定拒绝执行插入操作。
例:创建触发器,当用户向stu_Sum表中插入数据时,禁止操作,SQL语句如下:
CREATE TRIGGER Insert_forbidden
ON stu_Sum
AFTER INSERT
AS
BEGINRAISERROR('不允许直接向该表插入记录,操作被禁止',1,1)ROLLBACK TRANSACTION
END
单击“执行”按钮,即可完成触发器的创建,执行结果如图
验证触发器的作用,输入向stu_Sum表中插入数据的语句,从而激活创建的触发器,SQL语句如下:
INSERT INTO stu_Sum VALUES (5)
单击“执行”按钮,即可完成激活创建的触发器的操作,执行结果如下:
14.2.1.2 DELETE触发器
用户执行DELETE操作时,就会激活DELETE触发器,从而控制用户能够从数据库中删除的数据记录。触发DELETE触发器之后,用户删除的记录行会被添加到DELETED表中,原来表中的相应记录被删除,所以可以在DELETED表中查看删除的记录。
例:创建DELETE触发器,用户对students表执行删除操作后触发,并返回删除的记录信息,SQL语句如下:
CREATE TRIGGER Delete_Studen
ON students
AFTER DELETE
AS
BEGINSELECT ID AS 已删除学生编号,name,sec,age FROM DELETED
END
GO
单击“执行”按钮,即可完成触发器的创建。
与INSERT触发器过程相同,这里AFTER后面指定DELETE关键字,表明这是一个用户执行DELETE删除操作触发的触发器。
创建完成,执行一条DELETE语句触发该触发器,SQL语句如下:
DELETE FROM students WHERE id=1004
💡提示:
这里返回的结果记录是从DELETED表中查询得到的。
14.2.1.3 UPDATE触发器
UPDATE触发器是当用户在指定表上执行UPDATE语句时被调用的。这种类型的触发器用来约束用户对现有数据的修改。UPDATE触发器可以执行两种操作:更新前的记录存储到DELETED表;更新后的记录存储到INSERTED表。
例:创建UPDATE触发器,用户对students表执行更新操作后触发,并返回更新的记录信息,SQL 语句如下:
CREATE TRIGGER Update_Student
ON students
AFTER UPDATE
AS
BEGINDECLARE @stucount INT =0SELECT @stucount=count(*) FROM studentsUPDATE stu_SUM SET Number=@stucountSELECT id as 更新前学生编号,name as 更新前学生姓名 FROM DELETEDSELECT id as 更新后学生编号,name as 更新后学生姓名 FROM INSERTED
END
GO
单击“执行”按钮,即可完成触发器的创建。
创建完成后,执行一条UPDATE语句触发该触发器
UPDATE students SET name='赵云' WHERE id=1003
💡提示:
由执行过程可以看到,UPDATE语句触发触发器之后,可以看到DELETED和INSERTED两个表中保存的数据分别为执行更新前后的数据。
该触发器同时也更新了保存所有学生人数的stu_Sum表,该表中number字段的值也同时被更新。
14.2.2 创建DDL触发器
与DML触发器相同,DDL触发器可以通过用户的操作而激活。由其名称数据定义语言触发器是当用户只需数据库对象创建修改和删除的时候触发。对于DDL触发器而言,其创建和管理过程与DML触发器类似。创建DDL触发器的语法格式:
CREATE TRIGGER trigger_name
ON {ALL SERVER | DATABSE}
[WITH <ddl_trigger_option> [,...n]]
{FOR | AFTER} {event_type | event_group} [,...n]
AS {sql_statement [;][,...n] | EXTERNAL NAME <method specifier> [;]}
<ddl_trigger_option>::=[ENCRYPTION][EXECUTE AS Clause]
主要参数:
- DATABASE:将DDL触发器的作用域应用于当前数据库。
- ALL SERVER:将DDL或登录触发器的作用域应用于当前服务器
- event_type:指定激发DDL触发器的SQL事件的名称
下面以创建数据库或服务器作用域的DDL触发器为例,来介绍创建DDL触发器的方法,在创建数据库或服务器作用域的DDL触发器时,需要指定ALL SERVER参数。
例:创建数据库作用域的DDL触发器,拒绝用户对数据库中表的删除和修改操作,语句如下:
CREATE TRIGGER DenyDelete_mydbase
ON DATABASE
FOR DROP_TABLE,ALTER_TABLE
AS
BEGINPRINT '用户无权执行删除操作!'ROLLBACK TRANSACTION
END
GO
单击“执行”按钮,即可完成触发器的创建操作
其中:
- ON关键字后面的DATABASE指定触发器作用域;
- DROP_TABLE,ALTER_TABLE指定DDL触发器的触发事件,即删除和修改表;
- BEGIN END语句块,输出提示信息。
创建完成,执行一条DROP语句触发该触发器,SQL语句如下:
DROP TABLE persons
例:创建服务器作用域的DDL触发器,拒绝用户创建或修改数据库操作,语法如下:
CREATE TRIGGER DenyCreate_Allserver
ON ALL SERVER
FOR CREATE_DATABASE,ALTER_DATABASE
AS
BEGIN
PRINT'用户无权创建或修改服务器上的数据库!'
ROLLBACK TRANSACTION
END
GO
单击“执行”按钮,即可完成触发器的创建操作:
创建成功之后,依次打开服务器的“服务器对象”下的“触发器”结点,可以看到创建的服务器作用域的触发器DenyCreate_Allserver,
当用户试图创建或修改数据库时触发,禁止用户操作
14.2.3 创建登录触发器
登录触发器是在遇到LOGON事件时触发,LOGON事件是在建立用户会话时引发的。创建登录触发器的语法如下:
CREATE TRIGGER TRIGGER_NAME
ON ALL SERVER
[WITH <logon_trigger_option> [,...n]]
{FOR | AFTER} LOGON
AS {sql_statement [;] [,...n] | EXTERNAL NAME <method specifier>[;]}
<logon_trigger_option>::=[ENCRYPTION][EXECUTE AS Clause]
主要参数:
- trigger_name:用于指定触发器的名称,其名称在当前数据库中必须是唯一的。
- ALL SERVER:表示将登录触发器的作用域应用于当前服务器。
- FOR | AFTER:AFTER指定仅在触发SQL语句中指定的所有操作成功执行时触发触发器。所有引用级联操作和约束检查在此触发器触发之前也必须成功。当FOR是指定的唯一关键字时,AFTER是默认值。视图无法定义AFTER触发器。
- sql_statement:是触发条件和动作。触发条件指定附加条件,以确定尝试的DML、DDL或登录事件是否导致执行触发器操作。
- <method_specifier>:对于CLR触发器,指定要与触发器绑定的程序集的方法。该方法不得引用任何参数并返回void。class_name必须是有效的SQL SERVER标识符,并且必须作为具有程序集可见性的程序集中的类存在。
例:创建一个登录触发器,该触发器仅允许白名单主机名连接SQL SERVER服务器,语句如下:
CREATE TRIGGER MyHostsOnly
ON ALL SERVER
FOR LOGON
AS
BEGINIF(HOST_NAME() NOT IN ('ProdBox','QaBox','DevBox'))BEGINRAISERROR('You are not allowed to logon from this hostname.',16,1)ROLLBACKEND
END
单击“执行”按钮,即可完成登录触发器的创建。
设置登录触发器后,当用户再次尝试使用SSMS登录时,会出现错误提示,因为用户要连接的主机名并不在当前的白名单上。
📢提醒:
除非你已完全理解并了解上述登录触发器的使用,或者你已是一位资深老鸟,否则,不可轻易尝试登录触发器操作。
14.3修改触发器
当触发器不满足需求时,可以修改触发器的定义和属性,在SQL SERVER中可以通过两种方式进行修改:
- 先删除原来的触发器,再重新创建与之名称相同的触发器;
- 直接修改现有触发器的定义,修改触发器定义可以使用ALTER TRIGGER语句。
14.3.1 修改DML触发器
修改DML触发器语法如下:
ALTER TRIGGER schema_name.trigger_name
ON {table|view}
[with <dml_trigger_option> [,...n]]
{FOR | AFTER | INSTEAD OF}
{[INSERT][,][UPDATE][,][DELETE]}
[NOT FOR REPLICATION]
AS
{sql_statement [;] [,...n] | EXTERNAL NAME <method specifier [;]>}
<dml_teigger_option>::= [ENCRYPTION][EXECUTE AS Clause]
<method_specifier>::=assembly_name.class_name.method_name
除了关键字由CREATE换成ALTER之外,修改DML触发器的语句和创建DML触发器的语句完全相同。
例:修改Insert_Student触发器,将INSERT触发器修改为DELETE触发器,语句如下:
ALTER TRIGGER Insert_Student
ON students
AFTER DELETE
AS
BEGIN--先判断stu_Sum表是否存在,如果不存在,就IF OBJECT_ID(N'stu_Sum',N'U') IS NULL --创建表 stu_Sum,表中只有一个整型字段number,初始值为0CREATE TABLE stu_Sum(number INT DEFAULT 0)--定义变量@stuNumber为整型DECLARE @stuNumber INT--将students表的记录数赋值给变量SELECT @stuNumber=COUNT(*) FROM students--如果表stu_Sum中不存在记录IF NOT EXISTS (SELECT * FROM stu_Sum)--就向表中插入数据INSERT INTO stu_Sum(number) values (0)--更新学生总人数插入到表stu_Sum中UPDATE stu_Sum SET number=@stuNumber
END
单击“执行”按钮,即可完成对触发器的修改操作,这里也可以根据实际需求修改触发器中的操作语句内容,这里就不赘述。
14.3.2 修改DDL触发器
修改DDL触发器的语法如下:
ALTER TRIGGER trigger_name
ON {ALL SERVER | DATABSE}
[WITH <ddl_trigger_option> [,...n]]
{FOR | AFTER} {event_type | event_group} [,...n]
AS {sql_statement [;][,...n] | EXTERNAL NAME <method specifier> [;]}
<ddl_trigger_option>::=[ENCRYPTION][EXECUTE AS Clause]
<method_specifier>::=assembly_name.class_name.method_name
除了关键字由CREATE换成ALTER之外,修改DDL触发器的语句和创建DDL触发器的语句完全相同。
例:修改服务器作用域的DDL触发器,拒绝用户对数据库进行修改操作,语法如下:
ALTER TRIGGER DenyCreate_Allserver
ON ALL SERVER
FOR DROP_DATABASE
AS
BEGIN
PRINT'用户无权删除服务器上的数据库!'
ROLLBACK TRANSACTION
END
GO
单击“执行”按钮,即可完成DDL触发器的修改操作。
14.3.3 修改登录触发器
修改登录触发器的语法:
ALTER TRIGGER TRIGGER_NAME
ON ALL SERVER
[WITH <logon_trigger_option> [,...n]]
{FOR | AFTER} LOGON
AS {sql_statement [;] [,...n] | EXTERNAL NAME <method specifier>[;]}
<logon_trigger_option>::=[ENCRYPTION][EXECUTE AS Clause]
除了关键字由CREATE换成ALTER之外,修改登录触发器的语句和创建登录触发器的语句完全相同。
例:修改登录触发器MyHostsOnly,添加允许登录SQL SERVER服务器的白名单主机名为“‘UserBox’”,语法如下:
ALTER TRIGGER MyHostsOnly
ON ALL SERVER
FOR LOGON
AS
BEGINIF(HOST_NAME() NOT IN ('ProdBox','QaBox','DevBox','UserBox'))BEGINRAISERROR('You are not allowed to logon from this hostname.',16,1)ROLLBACKEND
END
单击“执行”按钮,即可完成登录触发器的修改操作。
14.4 管理触发器
对于触发器的管理,用户可以启用与禁用触发器、修改触发器的名称、还可以查看触发器的相关信息。
14.4.1 禁用触发器
触发器创建之后便已启用了,如果暂时不需要使用某个触发器,可以将其禁用。触发器被禁用后并没有删除,它仍然作为对象存储在当前数据库中。但是当用户执行触发操作(INSERT\DELETE\UPDATE)时,触发器不会被调用。禁用触发器可以使用ALTER TABLE语句或者DISABLE TRIGGER语句。
例:禁用Update_Student触发器,语句如下:
ALTER TABLE students
DISABLE TRIGGER Update_Student
也可以使用下面语句:
DISABLE TRIGGER Update_Student ON students
单击“执行”按钮,禁止使用名称为Update_Student的触发器。
以上两种方法的思路是相同的,指定要禁用的触发器的名称和触发器所在的表。两种选其一即可。
例:禁止使用数据库作用域的触发器DenyDelete_mydbase,语句如下:
DISABLE TRIGGER DenyDelete_mydbase ON DATABASE
单击“执行”按钮,即可禁用数据库作用域的DenyDelete_mydbase触发器,其中,ON关键字后面指定触发器的作用域。
14.4.2 启用触发器
被禁用的触发器可以通过ALTER TABLE语句或ENABLE TRIGGER语句重新启用。
例:启用Update_Student触发器,语句如下:
ALTER TABLE students
ENABLE TRIGGER Update_Student
也可以使用下面语句:
ENABLE TRIGGER Update_Student ON students
单击“执行”按钮,即可启用名称为Update_Student的触发器。
以上两种方法的思路是相同的,指定要启用的触发器的名称和触发器所在的表。两种选其一即可。
例:启用数据库作用域的触发器DenyDelete_mydbase,语句如下:
ENABLE TRIGGER DenyDelete_mydbase ON DATABASE
单击“执行”按钮,即可启用数据库作用域的DenyDelete_mydbase触发器,其中,ON关键字后面指定触发器的作用域。
14.4.3 修改触发器名称
用户可以使用sp_rename系统存储过程来修改触发器的名称。使用sp_rename系统存储过程重命名触发器与重命名存储过程相同。
例:重命名触发器Delete_Student为Delete_Stu,语法如下:
sp_rename 'Delete_Student','Delete_Stu'
单击“执行”按钮,即可完成触发器的重命名操作。
💡注意:
使用sp_rename系统存储过程重命名触发器,不会更改sys.sql_modules类别视图的definition列中相应对象名的名称,所以建议用户不要使用该系统存储过程重命名触发器,而是删除该触发器后,使用新名称重新创建相同的触发器。
14.4.4 使用sp_helptext查看触发器
因为触发器是一种特殊的存储过程,所以也可以使用查看存储过程的方法来查看触发器的内容,例如,使用sp_helptext\sp_help以及sp_depends等系统存储过程来查看。
例:使用sp_helptext查看Insert_student触发器的信息
sp_helptext Insert_student
单击“执行”按钮,即可完成查看触发器信息操作,查询结果与创建触发器时编写的代码一致。
14.4.5 在SSMS中查看触发器信息
在SSMS中,可以以界面方式查看触发器信息,具体操作步骤如下:
步骤1:使用SSMS登录到SQL SERVER服务器,在“对象资源管理器”窗口中,打开需要查看的触发器所在的数据表结点,在触发器结点下,选择要查看的触发器,右击鼠标,在弹出的快捷菜单中选择“修改”命令,或者双击该触发器;
步骤2:在查询编辑窗口中将显示创建该触发器的代码内容,同时也可以对触发器的代码进行修改;
14.5 删除触发器
当触发器不再需要使用时,可以将其删除,删除触发器不会影响其操作的数据表,而当某个表被删除时,该表上的触发器也同时被删除。
删除触发器有两种方式:一种是在SSMS中删除;一种是使用DROP TRIGGER语句删除。
14.5.1 使用SQL语句删除触发器
DROP TRIGGER语句可以删除一个或多个触发器,其语法如下:
DROP TRIGGER trigger_name [,...n]
其中,trigger_name为要删除的触发器名称。
例:使用DROP TRIGGER语句删除Insert_Student触发器。
DROP TRIGGER Insert_Student
单击“执行”按钮,删除该触发器。
例:删除服务器作用域的触发器DenyCreate_AllServer。
DROP TRIGGER DenyCreate_AllServer ON ALL Server
单击“执行”按钮,删除该服务器作用域触发器。
14.5.2 使用SSMS手动删除触发器
与前面介绍的删除数据库、数据表以及存储过程类似,在SSMS中选择要删除的触发器,选择弹出右键菜单中的“删除”命令,或者选中后按DELETE键进行删除。
在弹出的“删除对象”窗口中单击“确认”按钮,即可完成触发器的删除操作。
14.6 认识其他触发器
本小节作为了解,书上的内容我也不是能看懂,下面是结合网上查找的资料拼凑在一起写的,各位请带上批判的主观意识来学习,接受指正!
除前面介绍的常用触发器外,本节再来介绍一些其他类型的触发器,如:替代触发器,嵌套触发器与递归触发器等。
14.6.1 替代触发器
简单来说,替代触发器(INSTEAD OF Trigger)是一种特殊类型的触发器,它允许你“拦截”并“替换”原本要在表或视图上执行的 DML 操作(INSERT, UPDATE, DELETE)。
它的核心作用是:当系统尝试对某个对象执行一个 DML 操作时,不执行该操作,而是转而执行触发器内部定义的一套替代逻辑
对于替代(INSTEAD OF)触发器,SQL SERVER服务器在执行触发INSTEAD OF触发器的代码时,先建立临时的INSERTED和DELETED表,然后直接触发INSTEAD OF触发器,而拒绝执行用户输入的DML操作语句。
替代触发器最主要、最常见的应用场景是:为了让不可更新的视图变得可更新。
- 核心应用:处理复杂视图的更新。在关系型数据库中,有些视图是非常简单直接的(例如基于单个表的简单查询),你可以直接对它们进行 INSERT、UPDATE 或 DELETE 操作,数据库系统就知道如何将这些操作反向应用到基表上。但是,很多复杂视图,例如包含:多表连接(JOIN)、有计算的聚合函数(GROUP BY,SUM,COUNT...)等,如果你尝试直接对这些复杂视图进行写操作,数据库引擎会报错,因为它无法智能地判断出你的操作意图应该如何准确地映射回底层的哪一个或哪几个基表上。这时,替代触发器就上场了。你可以在视图上创建一个INSTEAD OF触发器,告诉数据库:“当有人视图向这个视图插入数据时,别自己瞎琢磨,按照我写的代码来执行就好。”
例:基于表User(UserId,UserName)和Orders(OrderId,UserId,Amount),我们创建了如下视图来显示订单及其用户信息:
CREATE VIEW VW_UserOrders
AS
SELECT u.UserId, u.UserName, o.OrderId, o.Amount
FROM Users u
INNER JOIN Orders o ON u.UserId = o.UserId
这个视图包含了JOIN,所以它是不可直接更新的。如果我们想通过这个视图插入一条新订单,直接INSERT会失败。
我们在视图VW_UserOrders上创建一个替代触发器来解决这个问题:
CREATE TRIGGER TR_VW_UserOrders_Insert
ON VW_UserOrders
INSTEAD OF INSERT
AS
BEGIN-- 触发器内部的替代逻辑:-- 1. 首先检查插入的数据中用户是否存在(根据UserName)-- 2. 如果不存在,可以先插入新用户(这里简化了,实际可能更复杂)-- 3. 然后,将订单数据插入到 Orders 表中,使用对应的 UserIdINSERT INTO Orders (UserId, Amount)SELECT u.UserId, i.AmountFROM inserted iINNER JOIN Users u ON i.UserName = u.UserName;-- 注意:这里假设 UserName 是唯一的,并且已存在
END
现在,当我们执行:
INSERT INTO VW_UserOrders (UserName, Amount) VALUES ('Alice', 99.99)
数据库不会直接向视图VW_UserOrders插入值,而是会执行替代触发器TR_VW_UserOrders_Insert中的代码,从虚拟表inserted 中获取到插入值('Alice', 99.99),然后根据逻辑向Orders表中插入一条正确记录。
- 在表上的特殊用途:虽然主要用于视图,但 INSTEAD OF 触发器也可以用在表上。这种情况较少见,通常用于实现非常规的约束或级联操作,或者在执行原始操作前必须进行某些极其复杂的校验和逻辑替换。
例如,你可以创建一个 INSTEAD OF DELETE 触发器,将删除操作替换为将记录标记为“已归档”或移动到历史表的操作(即“软删除”的逻辑强化版)。
特性 | 替代触发器(INSTEAD OF) | 后置触发器(AFTER) |
执行时机 | 代替原始操作执行。原始操作根本不会发生 | 在原始操作成功执行之后才执行 |
适用对象 | 主要用于视图,也可用于表 | 只能用于表 |
数量 | 每个操作(INSERT/UPDATE/DELETE)只能定义一个INSTEAD OF触发器 | 每个操作可以定义多个AFTER触发器 |
常见用途 | 使不可更新的视图变得可更新 | 实施更复杂的业务规则、审计日志、同步其他表等 |
总结:
是什么: | INSTEAD OF 触发器是一个“拦截器”和“替换器”。 |
干嘛用: | 主要目的是为了对复杂的、不可更新的视图执行 DML 操作。你通过编写自定义逻辑,明确告诉数据库如何将对视图的操作转化为对底层基表的操作。 |
核心价值: | 它提供了极大的灵活性,让你能够通过视图接口实现复杂的业务逻辑,而对前端应用程序来说,它好像只是在操作一个简单的表一样。 |
14.6.2 嵌套触发器
嵌套触发器指的是一个触发器(我们称之为“父触发器”)的执行,引发了另一个触发器(“子触发器”)的执行的情况。这种“连锁反应”可以一层套一层,形成嵌套。
简单来说,就是触发器中执行的操作,又会去触发其他的触发器。
触发器通常是在对表执行 INSERT、UPDATE 或 DELETE 操作之后(AFTER Trigger)或代替操作之前(INSTEAD OF Trigger)自动执行的。如果一个触发器的执行逻辑内部,又对另一张表(或甚至是同一张表,但需注意条件,详见“递归触发器”)进行了数据修改(DML操作),而这个修改操作恰好也定义了触发器,那么第二个触发器就会被激活。
这个过程可以继续下去,形成嵌套链。
一个简单的过程描述:
- 用户对 表A 执行了一个 UPDATE 操作。
- 这个操作激活了 表A 上的一个 AFTER UPDATE 触发器(触发器1)。
- 触发器1 的内部代码执行了一个对 表B 的 INSERT 操作。
- 这个对 表B 的 INSERT 操作,又激活了 表B 上的一个 AFTER INSERT 触发器(触发器2)。
- 触发器2 开始执行它的逻辑。
- 如果 触发器2 的内部代码又修改了 表C,而 表C 也有触发器,那么触发器3又会被激活...
- 以此类推。。。
这个过程就形成了一个触发器嵌套链。触发器1是触发器2的父触发器。
14.6.2.1 优缺点
优点:
- 自动化复杂业务逻辑:可以将复杂的、多步骤的业务规则分解到多个触发器中,实现高度的自动化。例如,更新库存后自动生成采购单,插入订单后自动计算折扣等。
- 数据一致性:确保跨越多个表的关联操作能够自动、强制地完成,减少了应用程序代码的负担和出错的可能性。
缺点:
- 性能问题:一个简单的原始操作可能导致大量隐藏的、级联的触发器执行,非常消耗系统资源,可能导致性能急剧下降且难以排查。
- 调试困难:当出现数据问题时,调试嵌套调用的触发器会非常复杂,很难理清整个调用链条和中间状态。
- 意外循环:最危险的情况是无限递归。例如,触发器A 更新了表B -> 激活了 触发器B,触发器B 又回过头来更新了表A -> 再次激活 触发器A,如此循环往复,直到数据库达到嵌套层级限制并报错终止。
14.6.2.2 如何管理和控制嵌套
由于潜在的风险,所有主流数据库系统都提供了管理嵌套触发器的机制。
- 嵌套层级限制
数据库通常有一个最大嵌套层级的配置选项(默认通常是开启的,并设置一个层数上限,如 SQL Server 默认是 32 层)。这是防止无限递归的安全网。当嵌套层数超过这个配置值时,操作将被终止并回滚,数据库会抛出错误。
在SQL SERVER中查看和设置:
-- 查看当前最大嵌套层数
EXEC sp_configure 'nested triggers'-- 设置最大嵌套层数(例如设为10)
EXEC sp_configure 'nested triggers', 10
RECONFIGURE
- 禁用嵌套触发器
你可以完全关闭整个数据库实例的嵌套触发器功能。关闭后,一个触发器执行过程中引发的其他触发器将不会被执行。
在SQL SERVER中禁用:
-- 禁用嵌套触发器 (1:禁用, 0:启用)
EXEC sp_configure 'nested triggers', 0
RECONFIGURE
- 在触发器内部进行条件判断
最推荐的方式是在编写触发器时保持谨慎。在触发器代码开始时,使用条件语句(如 IF)检查是否真的需要进行数据修改。例如,使用 UPDATE() 函数或检查 inserted/deleted 虚拟表,确保只有数据真正发生变化时才执行后续逻辑,这样可以避免不必要的嵌套触发。
总结:
是什么: | 嵌套触发器是由一个触发器的动作激活另一个触发器而形成的链式调用过程。 |
用途: | 用于自动化复杂的、跨表的业务逻辑,保证数据一致性。 |
风险: | 带来显著的性能开销、调试复杂度和无限递归的风险。 |
管理: | 数据库系统通过最大嵌套层级来防止无限循环,并允许DBA全局禁用嵌套功能。最佳实践是在触发器代码内部进行条件判断,避免不必要的触发。 |
14.6.3 递归触发器
递归触发器是嵌套触发器(Nested Triggers)的一个特殊且需要极其谨慎对待的子类。
核心定义:递归触发器指的是一个触发器在其自身的执行过程中,通过修改数据的行为,直接或间接地再次激活了它自己,从而形成了一种循环调用。
它可以分为两种类型:
- 直接递归(Direct Recursion)
- 场景:触发器作用于数据表A上。
- 过程:当数据表A发生数据变更时,触发器被激活。在它的执行代码中,它又对数据表A进行了第二次数据修改(INSERT/UPDATE/DELETE)。这第二次修改会再次激活同一个触发器。
- 总结:触发器A → 修改数据表A → 触发器A再次被激活
- 间接递归(Indirect Recursion)
- 场景:有两个触发器,分别作用于数据表A和数据表B上。
- 过程:数据表A上的触发器A被激活,其代码修改了数据表B。这个修改激活了数据表B上的触发器B。而触发器B的代码又回过头来修改了数据表A。这个对数据表A的修改再次激活了触发器A,从而形成一个循环。
- 总结:触发器A → 修改数据表B → 触发器B → 修改数据表A → 触发器A(循环开始)。
一个经典的直接递归场景示例
假设我们有一个表示员工层级关系的表Employees
EmployeelD | Name | ManagerID |
1 | Alice | NULL |
2 | Bob | 1 |
3 | Charlie | 2 |
现在我们想在表中添加一个DirectReportCount字段,实时记录每位员工有多少个直接下属。
EmployeelD | Name | ManagerID | DirectReportCount |
1 | Alice | NULL | 2 |
2 | Bob | 1 | 1 |
3 | Charlie | 2 | 0 |
4 | David | 1 | 0 |
我们可以创建一个DML触发器来实现这个逻辑:
触发器逻辑:
当一个员工的ManagerID被更新时(比如Charlie的经理从Bob换成了David),触发器需要:
- 减少原经理的DirectReportCount
- 增加新经理的DirectReportCount
那么,问题来了?
这个触发器是挂在Employees表上的,当它执行第一步时:
UPDATE Employees SET DirectReportCount=DirectReportCount-1 WHERE EmployeeID=2
这个UPDATE语句本身又会再次激活这个触发器!
因为触发器监听的是 UPDATE 操作,而它自己正在执行一个 UPDATE 操作。这就形成了直接递归。
递归触发器的巨大风险:无限循环
如果没有安全机制,递归触发器几乎必然导致无限循环。
在上面的例子中:
- 用户更新了Charlie的经理。
- 触发器激活,去更新Bob的计数。
- 更新Bob计数的操作,再次激活同一个触发器。
- 触发器再次运行,它可能又会尝试去更新某个计数...
- 这个过程会无限重复,直到系统资源耗尽。
数据库如何防止无限递归?
为了防止系统崩溃,所有主流数据库(如 SQL Server, PostgreSQL)都引入了安全措施:
- 递归层级限制(Recursion Limit):数据库允许递归发生,但会设置一个最大递归深度(例如,SQL Server 默认的直接递归最大深度为 32 层)。当递归超过这个层数时,数据库会主动终止事务并将其回滚,然后抛出一个错误。
- 默认禁用:在某些数据库(如 SQL Server)中,递归触发器功能是默认关闭的。这是一个安全特性,防止开发者无意中创建出危险的递归。
在 SQL Server 中管理递归触发器:
-- 查看当前数据库的递归触发器设置(is_recursive_triggers_on)
SELECT name, is_recursive_triggers_on FROM sys.databases WHERE name = 'YourDBName';-- 启用当前数据库的递归触发器
ALTER DATABASE YourDBName SET RECURSIVE_TRIGGERS ON;-- 禁用当前数据库的递归触发器
ALTER DATABASE YourDBName SET RECURSIVE_TRIGGERS OFF;
如何安全地使用递归触发器?
虽然危险,但在受控的情况下,递归触发器可以解决一些特殊问题(如维护层次结构数据的派生数据,如上例)。
安全使用的关键是在触发器内部设置递归终止条件。
修改上面的例子,使其安全:
我们可以在触发器开始时,检查当前递归是否是我们需要的。通常的方法是使用 TRIGGER_NESTLEVEL() 函数(在 SQL Server 中)或类似功能。
CREATE TRIGGER TR_Employee_UpdateCount
ON Employees
AFTER INSERT, UPDATE, DELETE
AS
BEGIN-- 关键:如果递归层级大于1,说明这是递归调用,直接返回,不再执行后续逻辑。-- 这样,只有最外层的第一次调用会执行核心逻辑,递归调用会被阻断。IF TRIGGER_NESTLEVEL(OBJECT_ID('TR_Employee_UpdateCount')) > 1RETURN;-- ... 这里写正常的业务逻辑:计算原经理和新经理的ID...-- ... 然后更新他们的 DirectReportCount ...-- 注意:由于我们上面的终止条件,这里的UPDATE语句不会再触发递归。UPDATE Employees SET DirectReportCount = ... WHERE EmployeeID = @OldManagerId;UPDATE Employees SET DirectReportCount = ... WHERE EmployeeID = @NewManagerId;
END
通过这种方式,我们有效地将递归“压扁”了,只有最外层的触发器调用会执行核心逻辑,内部的递归调用被立即终止,从而避免了无限循环。
总结:
是什么: | 递归触发器是一种会调用自身(直接或间接)的特殊触发器。 |
风险: | 最大的风险是无限循环,导致数据库性能急剧下降甚至崩溃。 |
安全机制: | 数据库通过递归深度限制和默认禁用来防止灾难发生。 |
如何使用: | 如果必须使用,务必在触发器内部设置明确的终止条件(如检查嵌套层级),以确保递归在可控范围内停止。 |
建议:尽量避免使用递归触发器。对于大多数需要维护层级或图状数据一致性的场景,可以考虑使用其他方案,如:
- 计划任务(定时批处理更新)
- 在应用程序层实现逻辑
- 使用数据库的专用功能(如 SQL Server 的计算列或索引视图,但不适用于所有情况)。
递归触发器是一个强大的工具,但正如“能力越大,责任越大”,它需要开发者具备审核的理解和谨慎的态度。