如何在 DB2 Universal Database 中暂时禁用触发器

转自:http://www.ibm.com/developerworks/cn/data/library/techarticles/0211yip/

请注意:在阅读本文前请先阅读 免责声明

简介

有时您会想暂时禁用表上的触发器。例如,尽管您可能需要触发器执行日常的 SQL 操作,但可能不希望在运行特定脚本时触发那些触发器。标准做法是删除触发器,当再次需要它时再重新创建,但如果您必须跟踪许多触发器,那就有点为难了。(现在,我该把那些触发器的源代码保存到哪里呢?)

本文提供了三种解决这个问题的方法:

每种方法都有其优缺点,但我们将这方面的 讨论留到文章末尾。

方法 1:对特定用户禁用触发器

用来执行数据库维护任务的用户标识通常与用于应用程序的用户标识不同,这种方法利用了这一情况。要使这种方法有效,您只需选择在不希望触发触发器时要使用的用户标识。

example1.db2 脚本中的 SQL 向您演示了这种方法。

设置

要设置这个示例:

  1. 创建两个表 t1  t2 。我们将在 t1 上创建一个样本触发器,它将引起对 t2 进行插入操作。
    	CREATE TABLE db2admin.t1 (c1 int)
    	CREATE TABLE db2admin.t2 (c1 int)

  2. 创建触发器:
    	CREATE TRIGGER db2admin.trig1
    	AFTER INSERT ON db2admin.T1
    	REFERENCING NEW AS o 
    	FOR EACH ROW MODE DB2SQL
    	WHEN (USER <> 'ADMINISTRATOR')
    	BEGIN ATOMIC
    		INSERT INTO db2admin.t2 values (o.c1);
    	END

这个触发器很简单。每当连接的用户标识(由 USER 专用寄存器返回的)与 ADMINISTRATOR 不匹配时,则将插入到 t1 中的值也插入到 t2 。因此,当不想触发触发器时,以用户 ADMINISTRATOR进行连接以执行您的任务。

测试示例
  1. 在创建了表 t1  t2 和触发器 trig1 之后,以不同于 ADMINISTRATOR 的任何用户进行连接并将值插入 t1 
    	INSERT INTO db2admin.t1 VALUES (111)

  2. 验证该值已由触发器复制到了表 t2 中:
    	SELECT * FROM db2admin.t2
    	C1
    	-----------
            	111
      	1 record(s) selected.
    

  3. 接下来,以用户 ADMINISTRATOR 连接,并尝试再次插入值:
    	INSERT INTO t1 VALUES (222)

  4. 验证表 t2 未更改,因为触发器未被激活:
    	SELECT * FROM db2admin.t2
    	C1
    	-----------
            	111
      	1 record(s) selected.

方法 2:用框架来允许禁用触发器

本节描述了一个触发器框架,您可以将它用于任何可能需要暂时禁用的触发器。使用框架要求触发器开发人员进行规划并对这种概念取得一致意见,但这样做的结果可以得到该问题非常清晰的解决方案。

example2.db2 脚本中的 SQL 向您演示了这种方法。

下面说明了这种机制的工作原理:

  • 定义触发器查找表 trigger_state ,它维护一个由触发器的名称和状态(active='Y' 或 'N')所组成的列表
  • 在定义触发器时,向 trigger_state 表添加一次查寻(在该触发器的 WHEN 子句中)以确定该触发器是否应该激活
设置

要设置这个示例:

  1. 创建两个表 t1  t2 。我们将在 t1 上创建一个样本触发器,它将引起对 t2 进行插入操作。
    	CREATE TABLE db2admin.t1 (c1 int)
    	CREATE TABLE db2admin.t2 (c1 int)

  2. 创建 trigger_state 表。
    	CREATE TABLE db2admin.trigger_state 
    	(
    		trigschema VARCHAR(128) not null,
    		trigname VARCHAR(30) not null, 
    		active char(1) not null
    	)
    

    乍一看,您很可能想在含有 trigschema  trigname 列的 trigger_state 表中放置一个主键。但是,我们稍后会在 性能优化中讨论优化问题。目前,我们先不在表上放置任何约束。

  3. 假定您想要在表 t1 上创建名为 trig1 的触发器。我们要做的第一件事情是向 trigger_state 表注册该触发器:
    	INSERT INTO db2admin.trigger_state VALUES ('DB2ADMIN','TRIG1','Y')
    

    提示:对所有值都使用 大写,与系统目录表保持一致。

  4. 接下来,为方便起见,我们将创建用户定义的函数(UDF)。当我们创建触发器时,它的用途将变得很明显:
    	CREATE FUNCTION db2admin.trigger_enabled (
    		v_schema VARCHAR(128), 
    		v_name VARCHAR(30))
    	RETURNS VARCHAR(1)
    	RETURN (SELECT active FROM db2admin.trigger_state 	WHERE trigschema=v_schema and trigname=v_name)
    

    重要:如果查寻失败,则这个函数返回空值。因此,确保正确填写 trigger_state 表,并在调用这个函数时传递正确的参数。

    如您所见,该函数将模式和触发器的名称作为输入,以在 trigger_state 表中执行查寻,并返回 active 列中的值。

  5. 创建触发器:
    	CREATE TRIGGER db2admin.trig1
    	AFTER INSERT ON db2admin.T1
    	REFERENCING NEW AS o
    	FOR EACH ROW MODE DB2SQL
    	WHEN (db2admin.trigger_enabled('DB2ADMIN','TRIG1') = 'Y')
    	BEGIN ATOMIC
    		INSERT INTO db2admin.t2 values (o.c1);
    	END
    

    这个触发器很简单。当启用它时,插入 t1 的值也会插入到 t2 。但是,在激活它之前,它调用 UDF trigger_enabled() 来确定该触发器是否被禁用。用该函数封装这个查询降低了出错的可能性,尤其是在需要创建许多触发器的情况下。

    提示:如果您的触发器已经将 WHEN 子句用于其它条件,则只需用 AND 操作符将条件串到一起。

测试示例
  1. 首先,我们测试该触发器是否按预期的方式工作:
    	INSERT INTO db2admin.t1 values (123)
    	DB20000I  The SQL command completed successfully.

  2. 验证 t2 也包含值 123,因为激活了触发器:
    	SELECT * FROM db2admin.t2
    	C1
    	-----------
            	123
      	1 record(s) selected.

  3. 现在,我们将禁用该触发器:
    	UPDATE db2admin.trigger_state SET active='N' 
    	WHERE trigschema='DB2ADMIN' and trigname='TRIG1'

  4. 然后将另一行插入 t1 
    	INSERT INTO db2admin.t1 values (456)

  5. 现在,让我们通过确定表 t2 未经更改来验证触发器已被禁用。
    	SELECT * FROM db2admin.t2
    	C1
    	-----------
            	123
      	1 record(s) selected.

重新启用触发器 

要重新启用触发器,只要再次设置触发器的状态。

	UPDATE db2admin.trigger_state SET active='Y' 
	WHERE trigschema='DB2ADMIN' and trigname='TRIG1'

性能优化

至此,我们还没有在 trigger_state 上创建任何唯一性约束或索引,因为有理由对此进行更彻底的讨论,另外还因为演示该技术时不需要这些约束或索引。

因为 trigger_state 可能维护数百甚至数千个触发器,所以我们希望使对该表执行查寻的开销最小化。与其在列 trigschema trigname 上创建主键(由它又可以创建唯一性索引),不如将创建这个唯一性索引作为单独的步骤,以便我们在该索引页中包含名为 active 的列。从基表取回多余的字节会占用额外的 I/O,这是对资源的浪费。

以下是该索引的定义,它用关键字 INCLUDE来指定在这个唯一性索引中添加 active 列:

	CREATE UNIQUE INDEX db2admin.trigstateIX 
		ON db2admin.trigger_state (trigschema, trigname) 
		INCLUDE (active)

如果使用 trigger_state 表来维护数千个触发器,则可能希望将这个表放在它自己的表空间中,并给它指定专门的缓冲池。这样,就可以将该查找表始终保存在内存中。尝试确定缓冲池的大小,以便确保 trigger_state 的所有行都在内存中,但不要使缓冲池太大以至浪费内存(您可以使用命令 LIST TABLESPACES SHOW DETAIL 的输出来帮助确定缓冲池的大小)。如果触发器数目在几千之内,则这种优化可能是不值得的,因为,假定 trigger_state 的行大小只有 41 字节左右(假定 trigschema 占 20 个字节, trigname 占 20 个字节,状态占 1 个字节),那么每 4 KB 的页能存储 100 个触发器的信息。

如果您有几千个触发器,请记住对 trigger_state 表运行统计。

当然,另一个重要的考虑事项是,只对需要定期禁用的触发器才使用这种技术。

方法 3:使用 SQL 存储过程维护触发器

 方法 1 方法 2中,我们描述了禁用触发器的方法,这样您就不必为删除和重建它们所引发出来的问题而担心了。在本节中,我们提供了一种解决方案,它使用 SQL 存储过程来封装和管理触发器的删除和重建。其机制是这样设计的:源代码始终存在于数据库中,因此不必跟踪触发器的源代码。

以下是这种机制的工作原理:

  1. 创建三个存储过程:
    • disable_trigger() — 禁用触发器
    • enable_trigger() — 启用触发器
    • show_disabled_triggers() — 显示所有已禁用的触发器
  2. 创建名为 trigtool.disabled_triggers 的表,它看上去类似于 syscat.triggers 系统目录表。这个表维护已删除触发器的副本。最初它是空的。
  3. 当需要禁用触发器时,调用 disable_trigger() ,它将触发器定义从 syscat.triggers 复制到 disabled_triggers 表,然后删除这个触发器。
  4. 要启用触发器,调用 enable_trigger() ,它从 trigtool.disabled_triggers 表重新创建触发器。

限制:不能通过这种方法禁用代码文本超过大约 30KB 的触发器。

用存储过程禁用和启用触发器提供了一种有别于实际删除和重建触发器的抽象级别。我们提供了实现该存储过程的所有源代码。但是,请确保阅读后面有关我们代码的 免责声明

设置

使用 trigtool 模式创建所有对象,并且脚本 example3.db2 提供了所有 DDL。

  1. 创建 32KB 页大小的缓冲区和 32KB 页大小的表空间。
    	CREATE BUFFERPOOL BP32K SIZE 1000 PAGESIZE 32K
    	CREATE TABLESPACE TS32K PAGESIZE 32K
    	MANAGED BY SYSTEM USING ('c:\ts32k\') BUFFERPOOL BP32K

  2. 创建 trigtool.disabled_triggers 表:
    	CREATE TABLE TRIGTOOL.DISABLED_TRIGGERS (
    		TRIGSCHEMA VARCHAR(128) not null,
    		TRIGNAME VARCHAR(128) not null,
    		TABSCHEMA VARCHAR(128) not null,
    		TABNAME VARCHAR(128) not null,
    		QUALIFIER VARCHAR(128) not null,
    		FUNC_PATH VARCHAR(254) not null,
    		TEXT VARCHAR(31500) not null
    		) in TS32K
    	ALTER TABLE TRIGTOOL.DISABLED_TRIGGERS 
    		ADD CONSTRAINT disabledtrig_pk PRIMARY KEY 
    		(trigschema, trigname)
    

    以下是这个表的一些重要特性:

    • 该表看起来几乎(但并非完全)与 syscat.triggers 相同。我们只包括了需要用来重新创建触发器的列。
    • 该表是在表空间 TS32K 中创建的,该表空间是 32KB 页大小
    • 在触发器模式与触发器名称上创建了主键约束
    • TEXT 列的类型是 VARCHAR(31500),这与 syscat.triggers 中使用 CLOB 类型的 TEXT 列不同。稍后讨论原因。
  3. 创建 trigtool.show_disabled_triggers() 过程,它提供了一个显示当前已禁用触发器的方法。基本上,它将已禁用游标的模式和名称作为一个游标返回给该过程的调用者,应用程序或用户可以从命令行处理器(CLP)检索它。以下是这个过程的源代码:
    	CREATE PROCEDURE TRIGTOOL.SHOW_DISABLED_TRIGGERS ()
    	LANGUAGE SQL
    	RESULT SETS 1
    	BEGIN
    	DECLARE c_triggers CURSOR WITH RETURN FOR
        		SELECT trigschema, trigname FROM TRIGTOOL.DISABLED_TRIGGERS;
    	OPEN c_triggers;
    	END

  4. 既然我们拥有查看已禁用触发器的方法,我们就可以创建一个名为 trigtool.disable_trigger() 的过程,它实际复制和删除触发器。
    	CREATE PROCEDURE TRIGTOOL.DISABLE_TRIGGER (
        		IN v_schema VARCHAR(128), 
        		IN v_name VARCHAR(128))
    	SPECIFIC DISABLE_TRIGGER
    	LANGUAGE SQL
    	BEGIN
    	DECLARE SQLCODE INT DEFAULT 0;
    	DECLARE v_stmt VARCHAR(250);
    	DECLARE EXIT HANDLER FOR NOT FOUND
        		SIGNAL SQLSTATE '80000' 
            		SET MESSAGE_TEXT='Trigger Not Found';
    	DECLARE EXIT HANDLER FOR SQLWARNING
        		SIGNAL SQLSTATE '80001' 
            		SET MESSAGE_TEXT='Unable to disable trigger';
    	INSERT INTO TRIGTOOL.DISABLED_TRIGGERS
    	SELECT
    		TRIGSCHEMA, TRIGNAME, TABSCHEMA, TABNAME,
    		QUALIFIER, FUNC_PATH, CAST(TEXT as VARCHAR(31500))
    	FROM SYSCAT.TRIGGERS
    	WHERE TRIGSCHEMA = v_schema and TRIGNAME = v_name
        		AND VALID='Y';
    	SET v_stmt =  'DROP TRIGGER ' || v_schema || '.' ||v_name;
    	EXECUTE IMMEDIATE v_stmt;
    	END

    该过程接收两个参数:要禁用的触发器的模式和名称。

    第一个操作是 INSERT,它从 syscat.triggers 表将信息复制到 trigtool.disabled_triggers 表。注: syscat.triggers 的 TEXT 列的 CLOB 数据类型被强制转化成了 VARCHAR(31500) 数据类型。复制完成之后,就用动态 SQL 删除触发器。因为没有定义异常处理程序,所以发生的任何错误都将导致回滚,从而使这个操作被拒绝。为了安全,声明一个 SQLWARNING 处理程序。这个处理程序发出 SQLEXCEPTION 信号,该信号将引起回滚。换句话说,只有在整个操作序列都完成,并不出现错误或警告的情况下,触发器才会被删除。

  5. 创建 trigtool.enable_trigger() ,它将从 trigtool.disabed_triggers 表重新创建触发器。
    	CREATE PROCEDURE TRIGTOOL.ENABLE_TRIGGER (
        		IN v_schema VARCHAR(128),
        		IN v_name VARCHAR(128))
    	LANGUAGE SQL
    	BEGIN
    	DECLARE SQLCODE INT DEFAULT 0;
    	DECLARE v_qualifier VARCHAR(128);
    	DECLARE v_func_path VARCHAR(1000);
    	DECLARE v_stmt VARCHAR(32672);
    	DECLARE v_curr_qualifier VARCHAR(128);
    	DECLARE v_curr_funcpath VARCHAR(1000);
    	DECLARE EXIT HANDLER FOR SQLWARNING
        		SIGNAL SQLSTATE '80000'
    		SET MESSAGE_TEXT = 'Error. Manual recreation required';
    	SET v_curr_qualifier = CURRENT SCHEMA;
    	SET v_curr_funcpath = CURRENT FUNCTION PATH;
    	SELECT qualifier, func_path, TEXT into v_qualifier, v_func_path, v_stmt
    	FROM TRIGTOOL.DISABLED_TRIGGERS
        		WHERE trigschema=v_schema and trigname=v_name;
    	SET v_func_path = 'SET CURRENT FUNCTION PATH = ' || v_func_path;
    	EXECUTE IMMEDIATE v_func_path;
    	SET v_qualifier = 'SET CURRENT SCHEMA = ' || v_qualifier;
    	EXECUTE IMMEDIATE v_qualifier;
    	EXECUTE IMMEDIATE v_stmt;
    	DELETE FROM TRIGTOOL.DISABLED_TRIGGERS
        		WHERE trigschema=v_schema and trigname=v_name;
    	SET v_curr_qualifier = 'SET SCHEMA = ' || v_curr_qualifier;
    	SET v_curr_funcpath = 'SET CURRENT FUNCTION PATH = ' || v_curr_funcpath;
    	EXECUTE IMMEDIATE v_curr_qualifier;
    	EXECUTE IMMEDIATE v_curr_funcpath;
    	END

    该过程(与前面那个删除触发器的过程类似)接收两个参数:要启用触发器的模式和名称。首先,存储当前会话的当前模式和当前函数路径,以便我们能够在该过程执行完成之后恢复它。然后,从 trigtool.disabled_triggers 表检索 qualifier, func_path text

    qualifier所包含的模式在最初创建触发器时限定未限定的表和视图。类似地, func_path值表示在最初创建触发器时使用的函数路径。函数路径用来解析触发器定义中可能存在的未限定函数。 text列包含用来创建触发器的原始文本。

    在恢复触发器之前,设置函数路径和当前模式值,以便在执行触发器文本时,对所有未限定的对象引用使用适当的限定符和函数路径。然后,使用 text,重新创建触发器,并 从 trigtool.disabled_triggers 删除该触发器的副本。您可以从代码中看出存在 30 KB 触发器文本大小限制的原因。EXECUTE IMMEDIATE 不支持 CLOB 类型作为参数,这就是我们必须将来自syscat.triggers 的原始文本从 CLOB 强制转化成 VARCHAR 的原因。

    最后,将当前模式和当前函数路径恢复成它们原先的值。

    就象 trigtool.disable_trigger() 一样,整个操作序列执行期间必须不出现任何错误或警告,否则整套操作都会回滚。

测试示例

在把一切都设置妥当之后,可以测试触发器的禁用和启用了。这个测试还提供了 example4.db2 脚本。要设置这个示例:

  1. 创建下列两个表:
    	CREATE TABLE db2admin.t1 (c1 int)
    	CREATE TABLE db2admin.t2 (c1 int)

  2. 测试该触发器是否按预期的方式工作:
    	INSERT INTO db2admin.t1 values (123)
    	DB20000I  The SQL command completed successfully.

  3. 验证 t2 是否也具有值 123,因为激活了触发器。
    	SELECT * FROM db2admin.t2
    	C1
    	-----------
    	        123
      	1 record(s) selected.

  4. 现在,我们将禁用该触发器:
    	CALL trigtool.disable_trigger('DB2ADMIN','TRIG1')

  5. 可以通过调用 trigtool.show_disabled_triggers() 来验证触发器已被禁用:
    	CALL trigtool.show_disabled_triggers()
    	Result set 1
    	--------------
    	TRIGSCHEMA                  TRIGNAME
    	--------------------------- --------------------
    	DB2ADMIN                    TRIG1
    	1 record(s) selected.
    	Return Status = 0

  6. 现在,将另一行插入 t1 
    	INSERT INTO db2admin.t1 values (456)	SELECT * FROM db2admin.t2
    	C1
    	-----------
            	123
      	1 record(s) selected.

    正如预料,表 t2 未发生改变,因为触发器已禁用。

重新启用触发器 
要重新启用触发器,只要用模式和触发器的名称来调用 enable_trigger() 存储过程,如下所示:

	CALL trigtool.enable_trigger('DB2ADMIN','TRIG1')

为了简化讨论,这个示例测试没有完整演示该存储过程的正确性。但是,在使用与缺省情况不同的模式和函数路径(即,用随机挑选的 CURRENT SCHEMA CURRENT FUNCTION PATH专用寄存器进行限定)创建触发器时,已经通过测试证明它工作正常。在脚本 example5.db2 中,提供了一个复杂情况下的测试用例。对于这个复杂案例的分析,我们留给读者作为一个练习。

结束语

本文演示了三种用于禁用和启用触发器的方法:通过用户、通过查找表和通过用存储过程管理触发器的删除和重新创建。每种技术都有其优缺点,而每种环境具有的不同要求,会造成某种方法比另一种优越。

表 1总结了每种方法的优缺点。

随时欢迎您的反馈,可通过 [email protected]将反馈发送给 Paul Yip。

表 1. 比较三种禁用和启用触发器的方法

方法 优点 缺点
对用户禁用
  • 不删除触发器
  • 无性能开销
  • 对于简单需求很理想
  • 必须使用特定的用户标识以避免激活触发器
触发器框架
  • 不删除触发器
  • 如果正在开发新触发器,则很理想
  • 需要触发器开发人员对使用框架取得一致
  • 少量的性能开销
存储过程
  • 无性能开销
  • 触发器源代码全部存在于数据库中
  • 通过存储过程达到了完全的抽象
  • 如果目前已经创建了许多触发器,则该方法很理想
  • 包可能需要自动重新联编
  • 触发器被实际删除
  • 创建 SQL 存储过程需要受支持的 C 编译器
  • 30KB 触发器大小的限制

致谢

作者要感谢 Serge Riealu 宝贵的审阅。他的建议使本文更为出色。

你可能感兴趣的:(database)