再上一篇:8.3完整性约束和事务
上一篇:8.4不好的事务习惯
主页
下一篇:8.7小结
再下一篇:9.1什么是 redo?
文章列表

8.6自治事务

Oracle 9i 10g编程艺术:深入数据库体系结构

自治事务(autonomous transaction)允许你创建一个“事务中的事务),它能独立于其父事务提交 或回滚。利用自治事务,可以挂起当前执行的事务,开始一个新事务,完成一些工作,然后提交或回滚, 所有这些都不影响当前执行事务的状态。自治事务提供了一种用PL/SQL控制事务的新方法,可以用于:
顶层匿名块; 本地(过程中的过程)、独立或打包的函数和过程; 对象类型的方法;
数据库触发器。
在 介绍自治事务如何工作之前,我想再强调一下,自治事务是一种功能非常强大的工具,但是如果 使用不当又会相当危险。真正需要自治事务的情况实际上极其少见。 我对使用了自治事务的代码都持怀疑 态度,这些代码需要多看几眼,要更仔细地审查。在使用自治事务的系统中偶然引入逻辑数据完整性问题 实在是太容易了。在下 面几节中,我们首先介绍自治事务如何工作,再讨论什么情况下可以安全地使用自 治事务。

8.6.1自治事务如果工作?

要展示自治事务的动作和结果,最好的办法是通过例子来说明。我们将创建一个简单的表来保存消息 :
ops$tkyte@ORA10G> create table t ( msg varchar2(25) );
Table created.

接下来创建两个过程,每个过程只是将其名字插入到消息表中,然后提交。不过,其中一个过程是正
常的过程,另一个编写为自治事务。我们将使用这些对象来显示在各种情况下哪些工作会在数据库中持久 保留(被提交)。
首先是AUTONOMOUS_INSERT过程:
ops$tkyte@ORA10G> create or replace procedure Autonomous_Insert
2 as
3 pragma autonomous_transaction;
4 begin
5 insert into t values ( 'Autonomous Insert' );
6 commit;
7 end;
8 /
Procedure created.

注意这里使用了pragma AUTONOMOUS_TRANSACTION。这个指令告诉数据库:执行这个过程时要作为一
个新的自治事务来执行,而且独立于其父事务。
注意 pragma是一个编译器指令,这是一种编辑器执行某种编译选项的方法。还有其他一些pragma。 参考PL/SQL编程手册,可以看到其索引中有pragma的一个列表。

以下是“正常”的NONAUTONOMOUS_INSERT过程:
ops$tkyte@ORA10G> create or replace procedure NonAutonomous_Insert
2 as
3 begin
4 insert into t values ( 'NonAutonomous Insert' );
5 commit;

6 end;
7 /
Procedure created.
下面来观察PL/SQL代码匿名块中非自治(nonautonomous)事务的行为:
ops$tkyte@ORA10G> begin
2 insert into t values ( 'Anonymous Block' );
3 NonAutonomous_Insert;
4 rollback;
5 end;
6 /
PL/SQL procedure successfully completed.
ops$tkyte@ORA10G> select * from t; MSG
------------------------- Anonymous Block
NonAutonomous Insert

可以看到,匿名块执行的工作(INSERT)由NONAUTONOMOUNS_INSERT过程提交。两个数据行都已提交 ,

所以ROLLBACK命令没有什么可以回滚。把这个过程与自治事务过程的行为加以比较:
ops$tkyte@ORA10G> delete from t;
2 rows deleted.
ops$tkyte@ORA10G> commit; Commit complete.

ops$tkyte@ORA10G> begin
2 insert into t values ( 'Anonymous Block' );
3 Autonomous_Insert;
4 rollback;
5 end;
6 /
PL/SQL procedure successfully completed. ops$tkyte@ORA10G> select * from t;
MSG
------------------------- Autonomous Insert
在此,只有自治事务中完成并已提交的工作会持久保留。匿名块中完成的INSERT由第4行的回滚语
句回滚。自治事务过程的COMMIT对匿名块中开始的父事务没有影响。本质上讲,中就抓住了自治事务的精 髓,并能从中了解到自治事务做什么。
总结一下,如果在一个“正常”的过程中 COMMIT,它不仅会持久保留自己的工作,也会使该会话中 未完成的工作成为永久性的。不过,如果在一个自治事务过程中完成COMMIT,只会让这个过程本身的工作 成为永久性的。

8.6.2何时使用自治事务?

Oracle 数据库在内部支持自治事务时间已经不短了。我们看到的一直都是以递归SQL形式出现的自 治事务。例如,从一个序列选择时就可以完成一个递归事务,从而在SYS.SEQ$表中立即递增序列。为支持 你的序列,SYS.SEQ$表的更新会立即提交,并对其他事务可见,但是此时你的事务(即父事务)尚未提交。 另外,如果回滚你的事务,序列的递增仍会保留;它没有随父事务回滚,因为这部分工作已经提交。空间 管理、审计以及其他内部操作都是以类似的递归方式完成的。
这个特性现在已经公开,任何人都能使用。不过我发现,在实际世界中,自治事务的合理使用实在很 有限。我多次看到,人们往往把自治事务当作某些问题的迂回解决方法,如触发器的变异表约束(变异表 也称为变化表,mutating table)。 不过,这几乎总会导致数据完整性问题,因为变异表就是为了读取触 发了触发器的表。不错,通过使用自治事务,确实可以查询表,但是你现在只是查询表,并不能 看到你的 修改(而这是变异表约束本来要做的事情;表正在修改当中,所以查询结果将不一致)。根据这个触发器的 查询所做的任何决定可能都有问题,你只是在读 取那个时间点的“旧”数据。
自治事务的一种可能合法的使用是用于定制审计,不过我要强调这只是“可能合法”。数据库中要对 信息完成审计,与编写定制的触发器相比,还有许多更高效的方法。例如,可以使用 DBMS_FGA包或者只是
使用AUDIT 命令本身。
对 此,应用开发人员经常问我:“怎么能对每一个修改安全信息的企图进行审计,并记录他们试图修 改的值呢?”他们不只是想避免修改企图的发生,还想为这些企图 建立一个永久的记录。在自治事务出现 之前,许多开发人员尝试着使用标准触发器(没有自治事务)来达到这个目的,但是失败了。触发器能检 测到UPDATE,而且发现一个用户在修改他不该修改的数据,此时就会创建一个审计记录,并使UPDATE失 败。遗憾的是,触发器让UPDATE失败时,它也会回滚审计记录,这是一个“全有或全无”性质的失败。有 了自治事务,现在就可以安全地捕获到企图完成的没有权限修改的数,而且已经对这个企图建立了一个记 录。
有意思的是,Oracle的AUDIT命令也提供了这个能力,多年前就可以使用自治事务捕获未成功的信 息修改企图。现在这个特性已经向Oracle开发人员公开,这样我们就能创建自己的更为定制的审计。
下 面是一个小例子。先在表上放一个自治事务触发器,它能捕获一个审计跟踪记录,详细地指出谁 试图更新表,这个人什么时候想更新表,另外还会提供一个描述性消 息指出这个人想要修改什么数据。这 个触发器的基本逻辑是:对于不向你直接或间接报告的员工,要防止更新这些员工记录的任何企图。
首先,从SCOTT模式建立EMP表的一个副本,以此作为本例使用的表:
ops$tkyte@ORA10G> create table emp
2 as
3 select * from scott.emp; Table created.
ops$tkyte@ORA10G> grant all on emp to scott;
Grant succeeded.

还要创建一个AUDIT_TAB表,在这个表中存储审计信息。注意,我们使用了列的DEFAULT属性,从而
默认具有当前登录的用户名以及登记审计跟踪信息的当前日期/时间:
ops$tkyte@ORA10G> create table audit_tab
2 ( username varchar2(30) default user,
3 timestamp date default sysdate,
4 msg varchar2(4000)
5 )
6 /
Table created.

接下来,创建一个EMP_AUDIT触发器对EMP表上的UPDATE活动进行审计:

ops$tkyte@ORA10G> create or replace trigger EMP_AUDIT
2 before update on emp
3 for each row
4 declare
5 pragma autonomous_transaction;
6 l_cnt number;
7 begin
8
9 select count(*) into l_cnt
10 from dual
11 where EXISTS ( select null
12 from emp
13 where empno = :new.empno
14 start with mgr = ( select empno
15 from emp
16 where ename = USER
)
17 connect by prior empno = mgr );
18 if ( l_cnt = 0 )
19 then
20 insert into audit_tab ( msg )
21 values ( 'Attempt to update ' || :new.empno );
22 commit;
23

24 raise_application_error( -20001, 'Access Denied' );
25 end if;
26 end;
27 /
Trigger created.
注意,这里使用了CONNECT BY查询。这会根据当前用户分析整个(员工)层次结构。它会验证我们
试图更新的记录是某个下属员工的记录,即这个人会在某个层次上向我们报告。 关于这个触发器的要点,主要如下:
PRAGMA AUTONOMOUS_TRANSACTION应用于触发器定义。整个触发器是一个“自治事务”,因 此它独立于父事务(即企图完成更新的事务)。
触发器在查询中从它保护的表(EMP表) 中具体读取。如果这不是一个自治事务,它本身 在运行时就会导致一个变异表错误。自治事务使我们绕开了这个问题,它允许我们读取表,但 是也带来了一个缺点, 我们无法看到自己对表做的修改。在这种情况下需要特别小心,这个逻 辑必须仔细审查。如果我们完成的事务是对员工层次结构本身的一个更新会怎么样?我们不会 在触发器中看到这些修改,在评估触发器的正确性时也要把这考虑在内。
触发器提交。这在以前不可能的,触发器以前从来不能提交工作。这个触发器并不是提交 父事务的工作(实际触发器触发的工作,即更新员工记录),而只是提交了触发器所完成的工作
(审计记录)。
在此,我们建立了EMP表,其中一个妥善的层次结构(EMPNO-MGR递归关系)。另外还有一个 AUDIT_TAB 表,要在其中记录修改信息的失败企图。我们的触发器可以保证这样一个规则:只有我们的经理或经理的 经理(依此类推)可以修改我们的记录。

下面尝试在EMP表中更新一条记录,来看看这是如何工作的:
ops$tkyte@ORA10G> update emp set sal = sal*10;
update emp set sal = sal*10
*
ERROR at line 1:
ORA-20001: Access Denied
ORA-06512: at "OPS$TKYTE.EMP_AUDIT", line 21
ORA-04088: error during execution of trigger 'OPS$TKYTE.EMP_AUDIT'

ops$tkyte@ORA10G> select * from audit_tab;
USERNAME TIMESTAMP MSG
--------- --------- ---------------------------------------- OPS$TKYTE 27-APR-05 Attempt to update 7369
触发器发现了情况,能防止UPDATE发生,而与此同时,会为这个企图创建一个永久记录(注意它在
AUDIT_TAB表的CREATE TABLE 语句上如何使用DEFAULT关键字来自动插入USER和 SYSDATE值)。接下来, 假设我们作为一个用户登录,想实际完成一个UPDATE,并做一些尝试:
ops$tkyte@ORA10G> connect scott/tiger
Connected.
scott@ORA10G> set echo on
scott@ORA10G> update ops$tkyte.emp set sal = sal*1.05 where ename = 'ADAMS';
1 row updated.
scott@ORA10G> update ops$tkyte.emp set sal = sal*1.05 where ename = 'SCOTT';
update ops$tkyte.emp set sal = sal*1.05 where ename = 'SCOTT'
* ERROR at line 1:
ORA-20001: Access Denied
ORA-06512: at "OPS$TKYTE.EMP_AUDIT", line 21
ORA-04088: error during execution of trigger 'OPS$TKYTE.EMP_AUDIT'

在演示表 EMP 的默认安装中,员工 ADAMS是 SCOTT 的下属,所以第一个 UPDATE 成功。再看第二个

UPDATE,SCOTT试图给自己加薪,但是由于SCOTT不向SCOTT报告(SCOTT不是自己的下属),所以这个更 新失败了。再登录回到包括AUDIT_TAB表的模式,可以看到以下结果:
scott@ORA10G> connect /

Connected.
ops$tkyte@ORA10G> set echo on ops$tkyte@ORA10G> select * from audit_tab;
USERNAME TIMESTAMP MSG
--------- --------- -------------------------------------
---
OPS$TKYTE 27-APR-05 Attempt to update 7369
SCOTT 27-APR-05 Attempt to update 7788
SCOTT 试图完成的这个UPDATE已经被记录下来。