八、PL/SQL中的事务处理一览
在这里,我们学习一下,如何使用事务处理的基本技术来保证数据库的一致性,这其中包括如何提交或取消对数据库的改动。Oracle管理下的工作或任务被称为会话。当我们运行应用程序或Oracle工具并连接到Oracle时,一个用户会话就会被开启。为了让用户会话可以"同步"工作并能共享计算机资源,Oracle就必须控制并发,所谓并发指的是多个用户同时访问同样的数据资源。要是没有合适的并发控制的话,就可能无法保证数据的完整性。也就是说,对数据的改变可能是在错误的秩序下完成的。
Oracle使用锁来控制并发访问数据。锁可以让我们临时占有某个数据库资源,如一个数据表或是表中的一条数据。这样,数据就不能被其他用户改变,直到我们结束对被锁定数据的处理。我们不需要显式地锁定一个资源,因为默认的锁机制会帮助我们保护数据和它的结构。但是,当我们想覆盖掉默认的锁时,我们就可以从多个锁模型中(如行共享和行排他)选出一个,发出请求为表或行加上我们选定的锁来替代默认的锁。
当两个或多个用户同时访问同一个模式对象时,就有可能发生死锁。比如说,两个用户要同时更新数据表,如果他们互相占有另外一个用户所要更新的资源,他们就会因得不到所需的资源而互相等待,直到Oracle向最后一个事务发出错误信号破除死锁为止。
当一个数据表在同一时刻被一个用户查询另一个用户更新时,Oracle就会为数据查询生成一个读一致的视图。一旦查询开始并继续执行的时候,被读取的数据是不会改变的。当更新活动执行时,Oracle会把数据表的数据和记录的变化内容放到回滚段中。Oracle利用回滚段建立读一致查询结果集并能在必要的时候取消所变化的内容。
1、如何用事务保护数据库
数据库事务是指作为单个逻辑工作单元执行的一系列操作。Oracle把一系列操作当作一个单元以便由语句引起的所有变动能够被一次性提交或回滚。如果在一个事务中某个环节执行失败,Oracle会自动地将数据内容恢复到执行前的状态。
程序中的第一条SQL语句会开启事务,当事务结束时,下一条SQL语句会自动地开启另一个事务。因此,每条SQL语句都是事务的一部分。一个分布式事务应该至少包含一条能够更新分布式数据库节点上的数据的SQL语句。
COMMIT和ROLLBACK语句能确保所有的数据库变化一次性提交,或一次性回滚。自上次提交或回滚之后的所有SQL语句又成为当前事务的一部分。SAVEPOINT语句能为当前事务处理中的当前点进行命名与标记。
2、使用COMMIT提交事务
COMMIT语句能终止当前事务,并把事务中的数据库变化提交到数据库中。在我们提交变化的内容之前,其他用户是无法访问到被修改了的数据;他们所看到的数据跟未修改之前的内容完全一样。
看一下事务的例子,假设把资金从一个银行的账户转入另一个银行的账户。这个事务需要做两次更新操作,借记第一个银行账户,然后借贷第二个银行账户。
BEGIN
??...
??UPDATE?accts
?????SET?bal?=?my_bal?-?debit
???WHERE?acctno?=?7715;
??...
??UPDATE?accts?
?????SET?bal?=?my_bal?+?credit
???WHERE?acctno?=?7720;
??COMMIT?WORK;
END;
COMMIT命令会释放作用于表和行的锁,也能清除自上一次提交或回滚之后的所有保存点。可选关键字WORK只是用于改善可读性而已。而关键字END代表了PL/SQL块的结束,而不是事务的结束。就像块可以跨越多个事务一样,事务也能跨越多个块。
可选关键字COMMENT能让我们为某个分布式事务添加注释。在提交的过程中如果出现了网络或机器故障,分布式事务的状态就未知或是有疑问(in- doubt)的了。那样的话,Oracle会在数据词典中保存COMMENT提供的文本内容和相关的事务ID。文本内容必须用引号夹起来的长度不超过50 字符的文字。如下例:
COMMIT?COMMENT?'In-doubt?order?transaction;?notify?Order?Entry';
PL/SQL不支持FORCE子句,这个子句在SQL中可以手工提交一个有疑问的(in-doubt)分布式事务。例如,下面的语句是不允许的:
COMMIT?FORCE?'23.51.54';???--?not?allowed?P257
3、使用ROLLBACK回滚事务
ROLLBACK语句能终止当前事务并放弃所有的数据变更。使用回滚有两个原因。第一,如果我们不小心误删了数据,回滚能帮助我们恢复原始数据。第二,如果我们开启了一个因异常或SQL语句执行失败而不能完成的事务,回滚就能让我们的数据回到最初状态,然后重新再执行一次。如下面的例子,我们把一个雇员的信息插入到三个不同的数据表中。如果插入过程中出现主键冲突,就会抛出DUP_VAL_ON_INDEX异常,这时,我们就可以在异常控制部分中使用事务回滚了。
DECLARE
??emp_id?INTEGER;
??...
BEGIN
??SELECT?empno,?...
????INTO?emp_id,?...
????FROM?new_emp?
???WHERE?...
??...
??INSERT?INTO?emp?VALUES?(emp_id,?...);
??INSERT?INTO?tax?VALUES?(emp_id,?...);
??INSERT?INTO?pay?VALUES?(emp_id,?...);
??...
EXCEPTION
??WHEN?DUP_VAL_ON_INDEX?THEN
????ROLLBACK;
??...
END;
- 语句级(Statement-Level)回滚
执行SQL之前,Oracle会标记一个隐式的保存点。然后,在语句执行失败的时候,Oracle就会自动执行回滚操作。例如,如果一条 INSERT语句因主键冲突而执行失败,语句就会被回滚。这时只有未执行成功的SQL所作的工作被丢弃。而那条语句之前执行成功的语句所作工作都会被保存下来。
Oracle还能回滚单条SQL语句并解除死锁,它会把错误发给参与执行的一个事务并回滚那个事务中的当前语句。
执行SQL语句之前,Oracle必须分析语法,确保语句满足语法规则并且语句内涉及到的模式对象都是有效的。语句执行时发现的错误能引起回滚操作,而分析时发现的错误不能引起回滚操作。
4、使用SAVEPOINT回滚部分事务
SAVEPOINT能为事务处理中的当前点进行命名和标记。使用ROLLBACK TO语句时,保存点能让我们恢复作了标记的事务的部分内容,而不是恢复整个事务。下例中,我们可以在插入操作之前标记一个保存点。如果INSERT语句要把一个重复的值插入字段empno,预定义异常DUP_VAL_ON_INDEX就会被抛出。那样,我们可以回滚到保存点,只有恢复插入操作。
DECLARE
??emp_id?emp.empno%TYPE;
BEGIN
??UPDATE?emp
?????SET?...?
???WHERE?empno?=?emp_id;
??DELETE?FROM?emp?WHERE?...
??...
??SAVEPOINT?do_insert;
??INSERT?INTO?emp?VALUES?(emp_id,?...);
EXCEPTION
??WHEN?DUP_VAL_ON_INDEX?THEN
????ROLLBACK?TO?do_insert;
END;
当我们回滚到一个保存点时,任何在那个保存点之后标记的保存点都会被擦除。但是,我们所回滚到的保存点不会被擦除。例如,如果我们标记了五个保存点,然后回滚到第三个,那么只有第四个和第五个保存点会被擦除。一个简单的回滚或提交都会擦除所有的保存点。
如果我们在一个递归子程序里标记了一个保存点,递归中每级都会有一个SAVEPOINT语句实例被执行。但是,我们只能回滚到最近标记的那个保存点。
保存点的名称是未声明的标识符并能在事务中反复使用。每次使用都会把保存点从它的旧位置移动到事务当前点。因此,回滚到保存点的操作只会影响到事务的当前部分。如下例所示:
BEGIN
??SAVEPOINT?my_point;
??UPDATE?emp?
?????SET?...?
???WHERE?empno?=?emp_id;
??...
??SAVEPOINT?my_point;???--?move?my_point?to?current?point
??INSERT?INTO?emp?VALUES?(emp_id,?...);
EXCEPTION
??WHEN?OTHERS?THEN
????ROLLBACK?TO?my_point;
END;
每一个会话中可用的保存点是没有限制的。一个有效的保存点就是一个自上一次提交或回滚之后的一个标记。
5、Oracle如何隐式回滚
在INSERT、UPDATE或DELETE语句执行之前,Oracle会标记一个隐式的保存点(对用户是不可用的)。如果语句执行失败, Oracle就会回滚到保存点。正常情况下,只有失败的SQL语句被回滚,而不是整个事务。但是,如果语句抛出了一个未捕获异常,主环境会决定回滚哪些内容。
如果我们的存储子程序因未捕获异常而退出,PL/SQL就不会为OUT模式参数进行赋值。并且,PL/SQL也不会对子程序所做的操作进行回滚。
6、终止事务
好的编程习惯是显式地执行提交或回滚每一个事务。是否在PL/SQL块或主环境中执行提交或回滚操作取决于程序的逻辑流程。如果我们没有显式地提交或回滚,主环境会决定它的最终状态。例如在SQL*PLUS中,如果PL/SQL块没有包含COMMIT或ROLLBACK语句,事务的最终状态就由块后的操作内容决定。如果我们执行一个数据定义,数据控制或COMMIT语句,或是调用EXIT,DISCONNECT或QUIT命令,Oracle都会提交事务。如果我们执行了ROLLBACK或退出SQL*PLUS会话,Oracle就会回滚事务。
在Oracle的预编译器环境中,如果程序非法结束,Oracle就会回滚事务。当程序显式地提交或回滚工作并使用RELEASE参数断开Oracle连接,它就能正常地退出:
EXEC?SQL?COMMIT?WORK?RELEASE;
7、使用SET TRANSACTION设置事务属性
我们可以使用SET TRANSACTION语句开启一个只读或只写的事务,建立隔离级别或把当前事务赋给一个指定的回滚段。只读事务对于运行那些涉及到一个或多个数据表的多查询来说,是很有用的;并且,在其他用户对表进行更新操作的时候,我们也可以对同样的表进行查询操作。
在只读事务中,所有的查询都会引用同一个提供多表,多查询,读一致视图的数据库快照。其他用户可以像平时一样继续查询或更新数据。在下面的例子中,作为一个商店经理,我们可以使用一个只读事务来收集过去一天、一周和一个月的销售量。在事务中,这些数字不会受到其他更新数据的用户的影响:
DECLARE
??daily_sales?????REAL;
??weekly_sales????REAL;
??monthly_sales???REAL;
BEGIN
??...
??COMMIT;???--?ends?previous?transaction
??SET?TRANSACTION?READ?ONLY?NAME?'Calculate?sales?figures';
??SELECT?SUM?(amt)
????INTO?daily_sales
????FROM?sales
???WHERE?dte?=?SYSDATE;
??SELECT?SUM?(amt)
????INTO?weekly_sales
????FROM?sales
???WHERE?dte?>?SYSDATE?-?7;
??SELECT?SUM?(amt)
????INTO?monthly_sales
????FROM?sales
???WHERE?dte?>?SYSDATE?-?30;
??COMMIT;???--?ends?read-only?transaction
??...
END;
SET TRANSACTION语句必须是只读事务中的第一条SQL语句,且只能出现一次。如果把事务设置成READ ONLY,后续查询就能看到事务开始之前提交的内容。使用READ ONLY并不会影响其他用户或事务。
- SET TRANSACTION的约束
只有SELECT INTO、OPEN、FETCH、CLOSE、LOCK TABLE、COMMIT和ROLLBACK语句才允许出现在只读事务中,并且查询过程不能使用FOR UPDATE。
8、覆盖默认锁
默认情况下,Oracle会自动地帮助我们锁定数据结构。但是,当覆盖掉默认的锁会对我们更加有利时,我们就可以发出请求为行或表添加特殊的数据锁。显式锁定能让我们在事务中共享数据表或拒绝对数据表的访问。
使用LOCK TABLE语句可以显式地锁住整张数据表;而SELECT FOR UPDATE可以锁定表中的特殊行,保证它们在更新或删除之前不会发生改变。但是,Oracle在执行更新或删除操作时会自动地获取行级锁(row- level locks)。所以,只在我们希望更新或删除操作执行之前锁住行才使用FOR UPDATE子句。
- 使用FOR UPDATE
当我们声明了一个被UPDATE或DELETE语句的子句CURRENT OF所引用的游标时,就必须使用FOR UPDATE子句来获取排它锁。如下例:
DECLARE
??CURSOR?c1?IS
????SELECT?????empno,?sal
??????????FROM?emp
?????????WHERE?job?=?'SALESMAN'?AND?comm?>?sal
????FOR?UPDATE?NOWAIT;
SELECT ... FOR UPDATE语句能够标记出那些将被更新或被删除的行,然后把它们一一锁定在结果集中。这在我们想对于行中已存在值进行修改时是很有用的。那样,我们就必须确定在更新行之前没有其他用户对它进行更改。
可选关键字NOWAIT能告诉Oracle,如果被请求行已经被其他用户锁定,那么就不需要等待了。控制权可以马上还给我们程序以便能够在重新获取锁之前做一些其他工作。如果不使用NOWAIT,Oracle会一直等待,直到能够访问到被锁定的行释放为止。
打开游标时,所有的行都会被锁住,而不仅仅是被取出的行。提交或回滚事务能够让行解除锁定。所以,我们不能在事务提交之后从FOR UPDATE的游标中取得数据。
查询多个数据表时,我们可以使用FOR UPDATE子句把行锁定限制在特定的表中。仅当FOR UPDATE OF子句引用到表中的一个字段的时候,该表中的行才会被锁定。例如,下面的查询就把行锁定在表emp,而不是dept:
DECLARE
??CURSOR?c1?IS
????SELECT????????ename,?dname
?????????????FROM?emp,?dept
????????????WHERE?emp.deptno?=?dept.deptno?AND?job?=?'MANAGER'
????FOR?UPDATE?OF?sal;
如下例所示,我们可以使用UPDATE或DELETE语句的CURRENT OF子句来引用从游标中取出的最新的行数据:
DECLARE
??CURSOR?c1?IS
????SELECT?????empno,?job,?sal
??????????FROM?emp
????FOR?UPDATE;
??...
BEGIN
??OPEN?c1;
??LOOP
????FETCH?c1
?????INTO?...
????...
????UPDATE?emp
???????SET?sal?=?new_sal
?????WHERE?CURRENT?OF?c1;
??END?LOOP;
END;
- 使用LOCK TABLE
我们可以使用LOCK TABLE语句把整张数据表用指定的锁模式进行锁定,这样就能共享或拒绝对这些表的访问。例如,下面的语句就把表emp用行共享的模式进行锁定。行共享锁允许并行访问数据表;它能阻止其他用户为了独占数据表而将整张表锁定。当事务提交或回滚后,锁就会被释放。
LOCK?TABLE?emp?IN?ROW?SHARE?MODE?NOWAIT;
锁的模式决定了什么样的其它锁可以作用于数据表上。例如,许多用户都可以同时获取一个表上的行共享锁,但只可能有一个用户获取排他锁。当其中一个用户获取的排他锁时,其他的用户就不能插入、删除或更新表中的数据了。
一个表锁从不会阻止用户对表进行查询,而且查询也不会获取表锁。只有两个不同的事务尝试修改同样的数据时,才可能出现其中一个事务等待另一个事务完成的现象。
- 提交后的数据取得
FOR UPDATE子句能获取排他锁。打开游标时所有的行都会被锁住,在事务提交后锁会被释放。所以,我们不能在事务提交后从使用了FOR UPDATE子句的游标中取得数据。如果这样做的话,PL/SQL就会抛出异常。下例中,游标FOR循环在第十次插入操作后会执行失败:
DECLARE
??CURSOR?c1?IS
????SELECT????????ename
?????????????FROM?emp
????FOR?UPDATE?OF?sal;
??ctr???NUMBER?:=?0;
BEGIN
??FOR?emp_rec?IN?c1?LOOP???--?FETCHes?implicitly
????...
????ctr??:=?ctr?+?1;
????INSERT?INTO?temp
?????????VALUES?(ctr,?'still?going');
????IF?ctr?>=?10?THEN
??????COMMIT;???--?releases?locks
????END?IF;
??END?LOOP;
END;
如果想在数据提交后也能取得数据,就不要使用FOR UPDATE和CURRENT OF子句。我们可以使用伪列ROWID模拟CURRENT OF子句。只要把每行的ROWID放到UROWID类型的变量中就可以了。然后在后续的更新和删除操作中用ROWID来辨识当前行。示例如下:
DECLARE
??CURSOR?c1?IS
????SELECT?ename,?job,?ROWID
??????FROM?emp;
??my_ename???emp.ename%TYPE;
??my_job?????emp.job%TYPE;
??my_rowid???UROWID;
BEGIN
??OPEN?c1;
??LOOP
????FETCH?c1
?????INTO?my_ename,?my_job,?my_rowid;
????EXIT?WHEN?c1%NOTFOUND;
????UPDATE?emp
???????SET?sal?=?sal?*?1.05
?????WHERE?ROWID?=?my_rowid;
????--?this?mimics?WHERE?CURRENT?OF?c1
????COMMIT;
??END?LOOP;
??CLOSE?c1;
END;
一定要注意,上面的例子中,被取得的记录并没有被锁住,因为我们没有使用FOR UPDATE子句。所以,其他用户可能无意地覆盖了我们所更新的内容。这样的话,游标就必须提供一个读一致的数据视图,而在更新中所使用的回滚段在游标关闭之前是不能被释放的。这就会降低行更新的处理速度。下面的例子演示了我们如何使用一个游标的%ROWTYPE属性,其中,游标引用了ROWID伪列:
DECLARE
??CURSOR?c1?IS
????SELECT?ename,?sal,?ROWID
??????FROM?emp;
??emp_rec???c1%ROWTYPE;
BEGIN
??OPEN?c1;
??LOOP
????FETCH?c1
?????INTO?emp_rec;
????EXIT?WHEN?c1%NOTFOUND;
????...
????IF?...?THEN
??????DELETE?FROM?emp
????????????WHERE?ROWID?=?emp_rec.ROWID;
????END?IF;
??END?LOOP;
??CLOSE?c1;
END;
九、使用自治事务完成单个逻辑工作单元
数据库事务是指作为单个逻辑工作单元执行的一系列SQL操作。通常,一个事务是由另外一个事务开启。在某些应用程序中,一个事务必须在开启它的事务的作用域之外进行操作。
自治事务是一个由其他事务(主事务)开启的独立的事务。自治事务可以把主事务挂起,然后执行SQL操作,在提交或回滚这些操作后,重新恢复主事务。下图是从主事务(MT)到自治事务(AT)然后返回的过程演示:
1、自治事务的优点
自治事务一旦被开启,就完全独立。它不与主事务共享任何锁、资源或提交依赖(commit-dependency)。所以,我们不能把事件记入日志,增加重试计数器等等,即使是主事务执行了回滚操作。
更重要的是,自治事务可以帮助我们建立模块化和可重用的软件组件。例如,存储过程可以在它们自己的自治事务中独立执行。应用程序不必知道过程的匿名操作,存储过程也无需知道应用程序的事务上下文。这就使自治事务比常规事务更不容易出错,使用更方便。
另外,自治事务具有常规事务的所有功能。他们可以并发查询,分布处理,并能使用所有的事务控制语句,其中也包括SET TRANSACTION。
2、定义自治事务
我们可以使用编译指示(编译器指令)AUTONOMOUS_TRANSACTION来定义自治事务。这个编译指示会让PL/SQL编译器把"程序"标记为自治的(独立的)。这里的术语"程序"包含:
- 顶级(Top-level,非嵌套)自治PL/SQL块
- 本地的、独立的或打包的函数和过程
- SQL对象类型的方法
- 数据库触发器
我们可以把这个指令放到程序声明部分的任何地方。但为了良好的可读性,一般把它放到声明的最顶部,语法如下:
PRAGMA?AUTONOMOUS_TRANSACTION;
在下面的例子中,我们把一个函数标记为自治:
CREATE?PACKAGE?banking?AS
??...
??FUNCTION?balance?(acct_id?INTEGER)
????RETURN?REAL;
END?banking;
CREATE?PACKAGE?BODY?banking?AS
??...
??FUNCTION?balance?(acct_id?INTEGER)
????RETURN?REAL?IS
????PRAGMA?AUTONOMOUS_TRANSACTION;
????my_bal???REAL;
??BEGIN
????...
??END;
END?banking;
约束:我们不能在一个包中使用这个编译指示来达到把所有的子程序(或对象类型中的所有方法)标记为自治的目的。只有独立的程序才能被标记为自治的。例如,下面这样的编译指示是不能使用的:
CREATE?PACKAGE?banking?AS
??PRAGMA?AUTONOMOUS_TRANSACTION;?--?not?allowed
??...
??FUNCTION?balance?(acct_id?INTEGER)?RETURN?REAL;
??END?banking;
在下面的例子中,我们再把一个独立的过程标记为自治:
CREATE?PROCEDURE?close_account?(acct_id?INTEGER,?OUT?balance)?AS
??PRAGMA?AUTONOMOUS_TRANSACTION;
??my_bal?REAL;
BEGIN?...?END;
下面,我们把一个PL/SQL块标记为自治:
DECLARE
??PRAGMA?AUTONOMOUS_TRANSACTION;
??my_empno???NUMBER?(4);
BEGIN
??...
END;
约束:我们不可以把嵌套PL/SQL块标记为自治。
在下面的例子中,我们把一个数据库触发器标记为自治。与常规的触发器不同的是,自治触发器能够包含事务控制语句,如COMMIT和ROLLBACK。
CREATE?TRIGGER?parts_trigger
??BEFORE?INSERT
??ON?parts
??FOR?EACH?ROW
DECLARE
??PRAGMA?AUTONOMOUS_TRANSACTION;
BEGIN
??INSERT?INTO?parts_log
???????VALUES?(:NEW.pnum,?:NEW.pname);
??COMMIT;???--?allowed?only?in?autonomous?triggers
END;
- 自治事务VS嵌套事务
虽然匿名事务是由另一个事务开启的,但它并不是一个嵌套事务:
- 它不与主事务共享事务资源。
- 它不依赖于主事务。例如,如果主事务回滚了,嵌套事务就会回滚,但自治事务不会。
- 它提交变化的内容对其他事务来说是立即可见的。(一个嵌套事务所提交的变化内容在主事务提交之前对其它事务是不可见的。)
- 自治事务中抛出的异常会产生事务级回滚,而不是语句级回滚。
- 事务关联文
如下图所示,主事务与嵌套程序共享它的关联文,但不与自治事务共享。同样,当一个自治程序调用另一个自治程序(或自我递归调用),程序也不会共享事务关联文。但是,当自治程序调用一个非自治程序的时候,程序会共享同一个事务关联文。
- 事务可见性
如图所示,自治事务在提交时它所做的内容变化对其它事务是可见的。当恢复到主事务的时候变化内容对主事务也是可见的,但这需要把它的隔离级别被设置为READ COMMITTED(默认)。
如果我们像下面一样把主事务的隔离级别设置为SERIALIZABLE,恢复主事务时,由它的自治事务所做的变化对主事务就不可见了。
SET?TRANSACTION?ISOLATION?LEVEL?SERIALIZABLE;
3、控制自治事务
一个自治程序中的第一个SQL语句会开启一个事务。当事务结束时下一个SQL语句又会开启另一个事务。自上一次提交或回滚后执行的SQL语句组成了当前事务。要控制自治事务,需使用下面的语句,它们只能应用到当前事务:
- COMMIT
- ROLLBACK [TO savepoint_name]
- SAVEPOINT savepoint_name
- SET TRANSACTION
COMMIT可以结束当前事务并把数据的变化保存到数据库中。ROLLBACK可以结束当前事务并放弃所有的数据变化,把数据恢复到未变化之前的状态。ROLLBACK还可以部分恢复事务。SAVEPOINT可以在一个事务中标记当前点;SET TRANSACTION能设置事务的属性,如读写访问和隔离级别。
要注意的是,设置在主事务中的属性并不会影响到它的自治事务。
- 进入与退出
当我们进入一个自治事务的执行部分时,主事务就被会挂起。当我们退出程序时,主事务就会恢复。要想正常地退出事务,我们就要显示地提交或回滚所有的自治事务。如果程序(或任何由它调用的程序)中含有状态无法确定的事务,就会有异常抛出,无法确定的事务就会被回滚。
- 提交与回滚
COMMIT和ROLLBACK会结束活动自治事务但不退出自治程序。如下图所示,当一个事务结束,紧接着的下一条SQL语句就会开启另一个事务。
- 使用保存点
保存点的作用域就是定义它的事务。定义在主事务中的保存点与定义在自治事务中的保存点没有任何关系。实际上,主事务和一个自治事务是可以使用相同的保存点名称的。
我们只能回滚到当前事务中标记的保存点。所以,当在一个自治事务中,我们不能回滚到主事务中标记的保存点。要是想这样做的话,我们就必须用已存在的自治程序恢复主事务。
在主事务中,如果回滚到一个在我们开启一个自治事务之前标记的保存点,那么回滚操作并不会影响到自治事务。记住,自治事务完全独立于主事务。
- 避免错误
为了避免一些公共错误,在设计自治事务时一定要记住以下几点:
- 如果一个自治事务要访问主事务(自治程序退出之前是不能恢复的)拥有的资源,就可能发生死锁。那样的话,Oracle就会在自治事务中抛出异常,如果异常未被捕获,主事务就会被回滚。
- Oracle初始化参数TRANSACTIONS指定了并行事务的最大数量。如果自治事务(与主事务并行执行)没有被考虑的话这个数字就有可能被超出。
- 如果我们没有使用提交或回滚操作退出一个活动自治事务,Oracle就会抛出一个异常。如果异常未被捕获,事务就会回滚。
4、使用自治触发器
很多时候,我们可以使用数据库触发器记下事件。假定我们要跟踪一个数据表所有的插入操作,即使是那些后来被回滚掉的。在下例中,我们用触发器把重复的行插入到一个影像表(shadow table)。由于是自治触发器,所以,触发器能把插入的内容提交到影像表中,无论我们是否把插入内容提交到主表中。
--?create?a?main?table?and?its?shadow?table
CREATE?TABLE?parts?(pnum?NUMBER(4),?pname?VARCHAR2(15));
CREATE?TABLE?parts_log?(pnum?NUMBER(4),?pname?VARCHAR2(15));
--?create?an?autonomous?trigger?that?inserts?into?the
--?shadow?table?before?each?insert?into?the?main?table
CREATE?TRIGGER?parts_trig
??BEFORE?INSERT
??ON?parts
??FOR?EACH?ROW
DECLARE
??PRAGMA?AUTONOMOUS_TRANSACTION;
BEGIN
??INSERT?INTO?parts_log
???????VALUES?(:NEW.pnum,?:NEW.pname);
??COMMIT;
END;--?insert?a?row?into?the?main?table,?and?then?commit?the?insert
INSERT?INTO?parts
?????VALUES?(1040,?'Head?Gasket');
COMMIT?;
--?insert?another?row,?but?then?roll?back?the?insert
INSERT?INTO?parts
?????VALUES?(2075,?'Oil?Pan');
ROLLBACK?;
--?show?that?only?committed?inserts?add?rows?to?the?main?table
SELECT?*?FROM?parts?ORDER?BY?pnum;
PNUM?PNAME
-------?---------------
1040?Head?Gasket
--?show?that?both?committed?and?rolled-back?inserts?add?rows
--?to?the?shadow?table
SELECT?*?FROM?parts_log?ORDER?BY?pnum;
PNUM?PNAME
-------?---------------
1040?Head?Gasket
2075?Oil?Pan
不同于常规触发器的是,自治触发器还能使用本地动态SQL执行DDL语句。下例中,触发器bonus_trig在表bonus更新后,删除临时表temp_bonus:
CREATE?TRIGGER?bonus_trig
??AFTER?UPDATE
??ON?bonus
DECLARE
??PRAGMA?AUTONOMOUS_TRANSACTION;???--?enables?trigger?to?perform?DDL
BEGIN
??EXECUTE?IMMEDIATE?'DROP?TABLE?temp_bonus';
END;
5、从SQL中调用自治函数
从SQL语句中调用的函数,必须遵守控制副作用的规则。为了检查是否与规则相冲突,可以使用编译指示RESTRICT_REFERENCES。它的作用是判断函数是否读写数据表或打包变量。
但是,在定义的时候,自治程序总不会与规则"不读数据库"(RNDS)和"不写数据库"(WNDS)相冲突,即使事实上是冲突的。这样的特性是很有用,在下面的例子中,当我们从查询中调用打包函数log_msg时,它能向数据表debug_output插入数据,而且还不与规则"不写数据库"冲突:
--?create?the?debug?table
CREATE?TABLE?debug_output?(msg?VARCHAR2(200));
--?create?the?package?spec
CREATE?PACKAGE?debugging?AS
??FUNCTION?log_msg?(msg?VARCHAR2)
????RETURN?VARCHAR2;
??PRAGMA?RESTRICT_REFERENCES?(log_msg,?WNDS,?RNDS);
END?debugging;
--?create?the?package?body
CREATE?PACKAGE?BODY?debugging?AS
??FUNCTION?log_msg?(msg?VARCHAR2)
????RETURN?VARCHAR2?IS
????PRAGMA?AUTONOMOUS_TRANSACTION;
??BEGIN
????--?the?following?insert?does?not?violate?the?constraint
????--?WNDS?because?this?is?an?autonomous?routine
????INSERT?INTO?debug_output
?????????VALUES?(msg);
????COMMIT;
????RETURN?msg;
??END;
END?debugging;
--?call?the?packaged?function?from?a?query
DECLARE
??my_empno???NUMBER?(4);
??my_ename???VARCHAR2?(15);
BEGIN
??...
??SELECT?debugging.log_msg?(ename)
????INTO?my_ename
????FROM?emp
???WHERE?empno?=?my_empno;
??--?even?if?you?roll?back?in?this?scope,?the?insert
??--?into?'debug_output'?remains?committed?because
??--?it?is?part?of?an?autonomous?transaction
??IF?...?THEN
????ROLLBACK;
??END?IF;
END;
十、确保PL/SQL程序的向后兼容
PL/SQL第二版允许使用一些不再使用的非正常功能:
- 声明变量时,可以对记录和表类型向前引用
- 在函数说明的RETURN子句中指定变量(非类型)名称
- 使用IN模式的参数为index-by表的元素赋值
- 把一个IN模式的记录中的一个字段作为另一个子程序的OUT模式参数
- 把一个OUT模式的记录中的一个字段放置于赋值符号的右边
- 在SELECT语句的FROM列表中使用OUT模式参数
为了向后兼容,我们可以设置PLSQL_V2_COMPATIBILITY标识来保留第二版的这些功能,在服务器端有两种方法设置这个标识:
- 把下面行添加到Oracle初始化文件中:
PLSQL_V2_COMPATIBILITY=TRUE - 执行下面的语句:
ALTER?SESSION?SET?plsql_v2_compatibility?=?TRUE;
ALTER?SYSTEM?SET?plsql_v2_compatibility?=?TRUE;
如果我们把标识指定为FALSE(默认的),就不能使用非正常功能。在客户端,命令行选项可以设置标识。例如,使用Oracle预编译程序,我们就可以在命令行中指定运行时选项DBMS。