Erasmo Acosta,Erasmo Acosta, IBM
Tony Lee, IBM
Paul Yip, IBM
2002 年 12 月 本文提供了三种方法,用来在 DB2 通用数据库 Linux版、UNIX 版和 Windows 版中暂时禁用触发器。
请注意:在阅读本文前请先阅读 免责声明。
简介
有时您会想暂时禁用表上的触发器。例如,尽管您可能需要触发器执行日常的 SQL 操作,但可能不希望在运行特定脚本时触发那些触发器。标准做法是删除触发器,当再次需要它时再重新创建,但如果您必须跟踪许多触发器,那就有点为难了。(现在,我该把那些触发器的源代码保存到哪里呢?)
本文提供了三种解决这个问题的方法:
- 方法 1:对特定用户禁用触发器
- 方法 2:用框架来允许禁用触发器
- 方法 3:使用 SQL 存储过程维护触发器
每种方法都有其优缺点,但我们将这方面的 讨论留到文章末尾。
方法 1:对特定用户禁用触发器
用来执行数据库维护任务的用户标识通常与用于应用程序的用户标识不同,这种方法利用了这一情况。要使这种方法有效,您只需选择在不希望触发触发器时要使用的用户标识。
example1.db2脚本中的 SQL 向您演示了这种方法。
设置
要设置这个示例:
- 创建两个表
t1 和 t2 。我们将在 t1 上创建一个样本触发器,它将引起对 t2 进行插入操作。
CREATE TABLE db2admin.t1 (c1 int)
CREATE TABLE db2admin.t2 (c1 int)
|
- 创建触发器:
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进行连接以执行您的任务。
测试示例
- 在创建了表
t1 、 t2 和触发器 trig1 之后,以不同于 ADMINISTRATOR 的任何用户进行连接并将值插入 t1 。
INSERT INTO db2admin.t1 VALUES (111)
|
- 验证该值已由触发器复制到了表
t2 中:
SELECT * FROM db2admin.t2
C1
-----------
111
1 record(s) selected.
|
- 接下来,以用户 ADMINISTRATOR 连接,并尝试再次插入值:
INSERT INTO t1 VALUES (222)
|
- 验证表
t2 未更改,因为触发器未被激活:
SELECT * FROM db2admin.t2
C1
-----------
111
1 record(s) selected.
|
方法 2:用框架来允许禁用触发器
本节描述了一个触发器框架,您可以将它用于任何可能需要暂时禁用的触发器。使用框架要求触发器开发人员进行规划并对这种概念取得一致意见,但这样做的结果可以得到该问题非常清晰的解决方案。
example2.db2脚本中的 SQL 向您演示了这种方法。
下面说明了这种机制的工作原理:
- 定义触发器查找表
trigger_state ,它维护一个由触发器的名称和状态(active='Y' 或 'N')所组成的列表
- 在定义触发器时,向
trigger_state 表添加一次查寻(在该触发器的 WHEN 子句中)以确定该触发器是否应该激活
设置
要设置这个示例:
- 创建两个表
t1 和 t2 。我们将在 t1 上创建一个样本触发器,它将引起对 t2 进行插入操作。
CREATE TABLE db2admin.t1 (c1 int)
CREATE TABLE db2admin.t2 (c1 int)
|
- 创建
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 表中放置一个主键。但是,我们稍后会在 性能优化中讨论优化问题。目前,我们先不在表上放置任何约束。
- 假定您想要在表
t1 上创建名为 trig1 的触发器。我们要做的第一件事情是向 trigger_state 表注册该触发器:
INSERT INTO db2admin.trigger_state VALUES ('DB2ADMIN','TRIG1','Y')
|
提示:对所有值都使用 大写,与系统目录表保持一致。
- 接下来,为方便起见,我们将创建用户定义的函数(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 列中的值。
- 创建触发器:
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 操作符将条件串到一起。
测试示例
- 首先,我们测试该触发器是否按预期的方式工作:
INSERT INTO db2admin.t1 values (123)
DB20000I The SQL command completed successfully.
|
- 验证
t2 也包含值 123,因为激活了触发器:
SELECT * FROM db2admin.t2
C1
-----------
123
1 record(s) selected.
|
- 现在,我们将禁用该触发器:
UPDATE db2admin.trigger_state SET active='N'
WHERE trigschema='DB2ADMIN' and trigname='TRIG1'
|
- 然后将另一行插入
t1 :
INSERT INTO db2admin.t1 values (456)
|
- 现在,让我们通过确定表
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 存储过程来封装和管理触发器的删除和重建。其机制是这样设计的:源代码始终存在于数据库中,因此不必跟踪触发器的源代码。
以下是这种机制的工作原理:
- 创建三个存储过程:
disable_trigger() — 禁用触发器
enable_trigger() — 启用触发器
show_disabled_triggers() — 显示所有已禁用的触发器
- 创建名为
trigtool.disabled_triggers 的表,它看上去类似于 syscat.triggers 系统目录表。这个表维护已删除触发器的副本。最初它是空的。
- 当需要禁用触发器时,调用
disable_trigger() ,它将触发器定义从 syscat.triggers 复制到 disabled_triggers 表,然后删除这个触发器。
- 要启用触发器,调用
enable_trigger() ,它从 trigtool.disabled_triggers 表重新创建触发器。
限制:不能通过这种方法禁用代码文本超过大约 30KB 的触发器。
用存储过程禁用和启用触发器提供了一种有别于实际删除和重建触发器的抽象级别。我们提供了实现该存储过程的所有源代码。但是,请确保阅读后面有关我们代码的 免责声明。
设置
使用 trigtool 模式创建所有对象,并且脚本 example3.db2提供了所有 DDL。
- 创建 32KB 页大小的缓冲区和 32KB 页大小的表空间。
CREATE BUFFERPOOL BP32K SIZE 1000 PAGESIZE 32K
CREATE TABLESPACE TS32K PAGESIZE 32K
MANAGED BY SYSTEM USING ('c:\ts32k\') BUFFERPOOL BP32K
|
- 创建
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 列不同。稍后讨论原因。
- 创建
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
|
- 既然我们拥有查看已禁用触发器的方法,我们就可以创建一个名为
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 信号,该信号将引起回滚。换句话说,只有在整个操作序列都完成,并不出现错误或警告的情况下,触发器才会被删除。
- 创建
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脚本。要设置这个示例:
- 创建下列两个表:
CREATE TABLE db2admin.t1 (c1 int)
CREATE TABLE db2admin.t2 (c1 int)
|
- 测试该触发器是否按预期的方式工作:
INSERT INTO db2admin.t1 values (123)
DB20000I The SQL command completed successfully.
|
- 验证
t2 是否也具有值 123,因为激活了触发器。
SELECT * FROM db2admin.t2
C1
-----------
123
1 record(s) selected.
|
- 现在,我们将禁用该触发器:
CALL trigtool.disable_trigger('DB2ADMIN','TRIG1')
|
- 可以通过调用
trigtool.show_disabled_triggers() 来验证触发器已被禁用:
CALL trigtool.show_disabled_triggers()
Result set 1
--------------
TRIGSCHEMA TRIGNAME
--------------------------- --------------------
DB2ADMIN TRIG1
1 record(s) selected.
Return Status = 0
|
- 现在,将另一行插入
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总结了每种方法的优缺点。
随时欢迎您的反馈,可通过 ypaul@ca.ibm.com将反馈发送给 Paul Yip。
表 1. 比较三种禁用和启用触发器的方法
| 方法 |
优点 |
缺点 |
| 对用户禁用 |
|
|
| 触发器框架 |
|
- 需要触发器开发人员对使用框架取得一致
- 少量的性能开销
|
| 存储过程 |
- 无性能开销
- 触发器源代码全部存在于数据库中
- 通过存储过程达到了完全的抽象
- 如果目前已经创建了许多触发器,则该方法很理想
|
- 包可能需要自动重新联编
- 触发器被实际删除
- 创建 SQL 存储过程需要受支持的 C 编译器
- 30KB 触发器大小的限制
|
致谢
作者要感谢 Serge Riealu 宝贵的审阅。他的建议使本文更为出色。
免责声明
本文包含了样本代码。IBM 授予您(“被许可方”)使用这个样本代码的非专有的、版权免费的许可证。然而,样本代码是以“按现状”的基础提供的,没有任何形式的(不论是明示的,还是默示的)保证,包括对适销性、适用于特定用途或非侵权性的默示保证。IBM 及其许可方不对被许可方使用该软件所导致的任何损失负责。任何情况下,无论损失是如何发生的,也不管责任条款怎样,IBM 或其许可方都不对由使用该软件或不能使用该软件所引起的收入的减少、利润的损失或数据的丢失,或者直接的、间接的、特殊的、由此产生的、附带的损失或惩罚性的损失赔偿负责,即使 IBM 已经被明确告知此类损害的可能性,也是如此。
作者简介
Erasmo Acosta在数据库行业有整整 15 年经验。他从事过多个领域,从软件开发和咨询到高级技术支持(Advanced Tech Support)和关键任务技术支持(Mission Critical Tech Support)。他作为 Informix ® 成员来到了 IBM。作为 IT 专家,他帮助客户从其它数据库迁移到 DB2 以及理解和利用 IBM 数据管理技术(Data Management Technology)的众多优点。可通过 erasmoa@us.ibm.com与他联系。 |
|
Tony Lee多年以来一直从事 DB2 系列产品的工作。这些年来,在 IBM 数据管理(Data Management)系列的许多产品方面,他担任过经理、计划人员和开发人员 — 从管理 IMS 到在 DataJoiner 中实现两阶段提交。Tony 目前是数据管理业务伙伴技术支持(Data Management Business Partners Technical Enablement)团队的高级顾问。他向 IBM 业务伙伴提供关于用于分布式平台的 DB2 的咨询和支持。他的特长是数据联邦和复制。可通过 tonylee@us.ibm.com与他联系。
|
|
Paul Yip是来自 IBM 多伦多实验室的 DB2 顾问,他的工作主要是协助 IBM 业务伙伴将应用程序从其它 DBMS 平台迁移到 DB2。他是 DB2 开发者园地的定期投稿人以及书籍 DB2 SQL Procedural Language for Linux, UNIX ®,and Windows ®的合著者。除了从事 DB2 方面的工作外,他还是 Linux 和开放标准的拥护者。可通过 ypaul@ca.ibm.com与他联系。
|
|