一种PL/SQL程序包失效的解决方案
2014-03-26彭秋生
彭秋生
(广东省邮政信息技术局 中国 广州 510898)
在应用程序与数据库交互的过程中,当数据量较大时,通常都提倡尽可能地使用存储过程(stored procedure),以减少交互过程产生的通信消耗。在oracle中,存储过程主要是指使用PL/SQL语言编写的子程序(subprogram),它包含了过程(procedures)、函数(function)和程序包(package,下文简称包)。在实际的程序设计过程中,程序员使用最多的就是程序包。这不仅因为包具有模块化、信息隐藏、效率高等优势,更重要的是它具有面向对象程序设计语言的特点[1]。包将逻辑相关的变量、常量、自定义数据类型、过程、函数、游标等PL/SQL块和域元素组织封装在一起,使用起来相当简洁和方便,与面向对象中的类具有异曲同工之妙。
在使用包的过程中,很多人都会遇到的一个令人讨厌的错 误 :“ORA-04068:existing state of packages has been discarded”。这个错误是Oracle认为程序包状态无效时抛出的。尽管oracle并不把它看作一种错误,而是更多地把它当作一种警告[2],但它的危害不小,需要仔细研究并加以解决。
1 错误的产生
我们先模拟错误的产生,整个模拟过程不妨称为“实验1”。
在oracle数据库中创建两个包:pkg_con和 pkg_app,前者只有包说明(specification,即包头),没有包的实现(body,即包体);后者既有包说明,又有包体[3]。pkg_app引用了pkg_con中的常量值。两个程序包的创建过程分别如下:
SQL>create or replace package pkg_con as
2 char_yes constant char:='2';
3 end pkg_con;
程序包已创建。
SQL>create or replace package pkg_app as
2 procedure prt;
3 end pkg_app;
程序包已创建。
SQL>create or replace package body pkg_app as
2 procedure prt is
3 begin
4 --输出pkg_con中的值
5 dbms_output.put_line(pkg_con.char_yes);
6 end prt;
7 end pkg_app;
程序包主体已创建。
显然,刚刚新创建的两个程序包状态都是正常的,没有任何问题。接下来在SQL*Plus中登录两个会话(session):会话A和会话B。会话B先连续一次或多次执行包pkg_app的过程prt,都不会发生错误。若会话A重建pkg_con包,会话B再执行过程pkg_app.prt,则会发生错误,如下实验1所示。
会话A
SQL>create or replace package pkg_con as
2 char_yes constant char:= ‘2’;
3 end pkg_con;
程序包已创建。
会话 B:
SQL>exec pkg_app.prt;
BEGIN pkg_app.prt; END;
*
ERROR位于第1行:
ORA-04068:已丢弃程序包的当前状态
ORA-04061:package“SCOTT.PKG_CON”的当前状态失效
ORA-04065:未执行,已更改或删除 package"SCOTT.PKG_CON"
ORA-06508:PL/SQL:无法在调用之前找到程序单元
ORA-06512:在"SCOTT.PKG_APP",line 4
ORA-06512:在line 1
从oracle给出错误描述看,发生错误的原因是当前执行包的状态失效了。包的状态可以在user_objects数据字典视图中查询到[4]。如果在重建包pkg_con之后,立即查一下包pkg_app的状态,就会发现包体状态已变成了无效(INVALID)。
2 对象依赖与自动重新编译机制
正常情况下,除了本身的语法、语义问题,包失效最常见原因是它所引用的对象发生了变化,这是由oracle的对象依赖机制决定[5]。oracle数据字典会实时跟踪各种对象以及对象之间相关性。包重建后,与之相关的其它对象的状态会立即变为失效;同时这种失效还会立即反映到其它会话中,导致其它会话中本地存储的对象副本状态也立即变为失效。在实验1中,包pkg_app引用了包pkg_con中的常量值,两者之间存在依赖关系。会话A对包pkg_con进行重建,数据字典实时跟踪到这一情况,马上将相关包pkg_app的状态置为失效,同时立即找到pkg_app所在的会话B,将会话B中存储在本地的副本也置为失效,最终导致了此次“ORA-04068”错误的发生。
但是,从另一个方面来说,oracle是具有自动重新编译机制的[5]。在执行程序包之前,oracle会先检查包的状态,若发现包的状态为 “无效”(INVALID),oracle会自动先对包进行重新编译,再运行程序包。若重新编译成功,程序包仍会正确地运行。如下实验2所示。
会话A:
SQL>create table test
(cstm_id varchar2(10));
表已创建。
SQL>create package pkg_test as
2 procedure insdata;
3 end pkg_test;
程序包已创建。
SQL>create package body pkg_test as
2 procedure insdata is
3 begin
4 insert into test values ('0001');
5 commit;
6 end insdata;
7 end pkg_test;
程序包主体已创建。
会话B:
SQL>exec pkg_test.insdata;
PL/SQL过程已成功完成。
会话A:
SQL>alter table test add (cstm_nm varchar2(60));表已更改。
会话B:
SQL>select object_name, object_type,timestamp, status
from user_objects where object_name='PKG_TEST';
OBJECT_NAME OBJECT_TYPE STATUS
------------- ------------ -------
PKG_TEST PACKAGE BODY INVALID
SQL>exec pkg_test.insdata
PL/SQL过程已成功完成。
SQL>select object_name, object_type, status
2 from user_objects where object_name='PKG_TEST';
OBJECT_NAME OBJECT_TYPE STATUS
------------------------------- ---------
PKG_TEST PACKAGE BODY VALID
在上述实验2中,我们顺序执行了以下几个步骤:
1)会话A创建数据表 test和包pkg_test,包pkg_test包含一个过程insdata,往表test中插入一条记录。显然,包pkg_test是依赖于表test的。
2)会话B执行pkg_test.insdata往表 test中插入一条数据,运行正常。
3)会话A更改test的表结构。此时,包pkg_test的包体状态变成了“无效”。
4)会话B执行pkg_test.insdata往表 test中插入一条数据,运行正常,没有因为包体失效而抛出“ORA-04068”错误。此时包体的时间戳已更新,表明包自动进行了重新编译。
上述实验第3)4)步证实了oracle自动重编译的机制的存在。但是,有了oracle自动重新编译机制的保证,为什么实验1中还有报错发生呢?这还与包的运行机制以及包中的全局变量和全局常量有关。
3 带全局变量/常量的包重新编译机制
与单个SQL语句相对,包之所以运行效率高,除了它大量减少了客户端与服务器之间的通信等原因外,另外一个重要的原因是包执行时不需要进行语法检查、语法检查、中间代码生成等解析过程。这一过程已经在包创建时完成了,称为包编译过程。当包第一次被调用(称为实例化)时,oracle将从磁盘读入该包的中间代码并将其放入系统全局工作区(SGA)中的共享缓冲区(shared pool)[6],但是,包的运行状态(即变量和游标信息)存放在用户全局区(UGA)中的会话存储区。这就保证了每个会话都有自己程序包运行状态的副本,即使不同的会话同时运行同一个程序包,也能做到互不干扰。
全局变量是指在包头中声明的变量,它作为程序包运行状态一部分,同样存放在用户全局区(UGA)的会话存储区中,与会话具有相同的生命周期。在同一个会话中全局变量会维持它的最新值,但全局变量的值设置、修改、维护对其它的会话是不会有影响的。下面的实验3说明了这一情况。
会话A:
SQL>SQL>create or replace package pkg_add as
2 g_num number:=100;
3 procedure add_num;
4 end pkg_add;
程序包已创建。
SQL>create or replace package body pkg_add as
2 procedure add_num is
3 begin
4 g_num:=g_num+1;
5 dbms_output.put_line( 'g_num='||g_num);
6 end add_num;
7 end pkg_add;
程序包主体已创建。
会话 B:
SQL>set serverout on
SQL>exec pkg_add.add_num;g_num=101
PL/SQL过程已成功完成。
会话 C:
SQL>set serverout on
SQL>exec pkg_add.add_num;
g_num=101
PL/SQL过程已成功完成。
实验3中会话B修改了全局变量g_num的值对会话C没有影响,因为全局变量在不同的会话中保存在不同的会话存储区,有各自独立的副本。
但是,全局变量和全局常量有一个非常重要的特性:如果定义全局变量的包进行了重建,那么这些全局变量的值将被重置为初始值。正是这个特性引起了“ORA-04068”错误,可以从两个方面对此进行说明。
一方面,由于对象依赖关系,一旦全局变量/常量所在包进行了重建,那么所有与这个包相关的包也会失效。当再次执行相关的包时,oracle会进行自动重新编译,这就意味其它会话要重新获取全局变量的值。
另外一方面,每个会话都是按自己的逻辑来处理各自会话存储区中全局变量的值,而自动重编译又会导致全局变量值的重新初始化,会覆盖了本地的值。此时,oracle若再“默默”地自动重编译,会导致本地全局变量的值变得不可控,因为它不再是以前的值了,而是被重置成了初始值。因此,oracle必须采取抛出错误的办法来显式地通知用户会话,否则,用户没办法知道全局变量的值已发生了变化。从这个角度来说,认为“ORA-04068”错误是一种警告而不是一种错误的观点也是有道理的,它的确更多的像一种警告。
4 解决办法
全局变量引起的错误影响可大可小,与所处理数据的重要性和数据量有关。如果“实验1”中过程pkg_app.prt是长时间的大量复杂的数据处理,会话B正好调用pkg_app.prt处理了一半的时候出错,很可能会造成无法挽回的损失。在更坏情况下,若很多包都与全局变量或全局常量的所在包有依赖关系,同时都发生“ORA-04068”错误,可能产生灾难性的后果。
事实上,实验1中是全局常量,不存在值不可控的问题,oracle完全可忽略这种错误继续处理下去。另外一方面,在程序设计过程中,全局常量应该考虑避开这种出错的风险。 因此在什么情况下使用全局变量,怎么避免或降低错误的发生,成为急需解决的问题。
1)避免使用全局变量/常量,使用函数代替全局常量
对于全局变量,应该尽量避免使用。如果确实需要用到全局变量,可以考虑通过数据表记录的方式来解决。把所有全局变量采取键值对的方式记录在数据表中。每个会话每次操作前先去数据表查询一下全局变量的最新值,操作完成后将新的结果更新到数据库表。全局变量是会话级别的,同一个会话的一般不会产生并发操作,数据库表记录的方式能有效的实现全局常量的功能。但缺点是,多个会话同时进行操作时,每个会话都需建立独立的键值对,否则不同会话间会相互影响。
对于全局常量,几乎所有的系统都需要定义。如果系统中不允许定义常量,那么就会导致很差的代码。但全局常量完全可以使用函数来替代,利用oracle自动重编机制来避免出现“ORA-04068”错误。我们将实验1中的pkg_con进行改造,如下所示。
SQL>create or replace package pkg_con as
2 function char_yes return char;
3 end pkg_con;
程序包已创建。
SQL>create or replace package body pkg_con as
2 function char_yes return char is
3 begin
4 return'2';
5 end char_yes;
6 end pkg_con;
程序包主体已创建。
只要函数名与原全局常量名一样,相关的引用包完全不需要做任何更改就可以继续使用。改造后的pkg_con无论怎样重建,pkg_app都能被oracle自动重编机制透明处理,不会出现任何问题。这种方法的缺点是要维护更多的代码,不像全局常量那么简洁。因此,可能需要对代码改造的工作量需要进行评估。若是系统中多处定义了全局常量,可能导致大量的代码修改,这不是一蹴而就的事情。但这是一个一劳永逸的方案,即使在短时间内无法全部改造完毕,也应该把它作为一个“长治久安”的首选策略。
2)集中定义全局变量和常量,尽量减少定义包的变动
首先,全局变量和常量应该全部集中定义,尽量定义在一个或几个专用包中。这跟C语言中的头文件有点类似。全局变量或常量的集中定义有利于变量(常量)地统一组织、管理和维护,更重要的是能够有效的降低“ORA-04068”错误发生的可能性。只有全局变量和常量的定义包发生变化才可能引发“ORA-04068”错误,其它引用包发生变化都会被oracle透明地自动重编译处理。当然,包头中的函数或过程发生变化导致自动重编译失败时例外。集中定义的另一个好处就是用户明确知道他正在修改的包是一个全局变量和常量定义包,必须记得而不是忘记对相关的引用包进行处理。
其次,全局变量或常量定义包应该避免频繁的变动。全局变量/常量定义包的变化必然会导致引用包的失效,需要对所有这些引用包进行重新编译处理。而集中定义的结果会导致同一个定义包的引用包数量增多,影响加大,定义包变动导致需要重新编译的包的数量也更多,工作量也很大。因此,最好能一次性定义好所有的全局变量和常量,尽量减少定义包的变动。
3)监测ORA-0408错误并重新执行包的存储过程
这个解决方法将处理错误的责任放到了客户端。它的思想是Oracle生成错误ORA-04068给客户端时提供了关于包状态已经失效的信息,由客户端来监测这个错误以及做出反应。客户端可以选择重新执行这个存储过程,如果它需要的话。这与oracle的建议是一致的。这里不再举例说明。当然,我们也可以考虑将包的调用处理封装成一个独立的过程或函数(c++/java中为类),专门对发生此类错误的包进行重新执行处理,以供其它过程或函数进行透明调用。值得注意的是,程序员应该对包重新执行的可行性有充分的考虑,毕竟并不是所有的包在所有的情况下都是可重复执行的。
5 结束语
本文由浅入深对全局变量引起包失效的错误原因进行了较为深入的剖析,它不仅与oracle的对象依赖机制有关,还与自动重编机制以及程序包的运行机制有关。 针对这种错误,本文提出了几种可行的解决方法,每一个解决方法都有各自的优点和不足,多种方法相结合才能达到满意的效果。对于新开发的程序包,应尽量避免使用全局变量,使用函数来代替全局常量;对于现有的程序包,应逐步实施代码改造,同时密切监控错误的发生、定期检测失效的程序包并自动进行重新编译处理。
[1]Feuerstein S,Pribyl B.Oracle PL/SQL程序设计[M].张晓明译.5版.北京:人民邮电出版社,2011:593-594.
[2]chinak62.重编译定义全局变量的包引发的ora-04068错误[EB/OL]. (2010) http://blog.itpub.net/180324/viewspace-660315/.
[3]Russell J,Portfolio T.PL/SQL User’s Guide and Reference Release 2(9.2)[M].California:Oracle Corporation,2002.
[4]盖国强.深入浅出Oracle-DBA入门、进阶与诊断案例[M].北京:人民邮电出版社,2006:99-101.
[5]Mcdonald C.精通Oracle PL/SQL[M].蔡伟毅,译.北京:人民邮电出版社,2009:47-53.
[6]Alapati S R.Oracle10g数据库管理艺术[M].钟鸣,等译.北京:人民邮电出版社,2007.