转载自:http://database.ctocio.com.cn/tips/14/8106014.shtml
在Oracle 里,如果你想编写存储过程你当然应该使用PL/SQL包。在这篇文章里,假设你一般了解PL/SQL 和非常熟悉PL/SQL 包。这篇文章关注于一个令人讨厌的错误,这个错误使许多使用PL/SQL以及使用API(例如JDBC)从应用层调用它的开发人员很苦恼。
【IT专家网独家】
导言
在我们需要与数据库进行交互时,应尽可能地使用存储过程——无论我们使用哪个数据库。这是假设这个数据库提供了编写存储过程的工具,大多数主要的数据库都确实如此,例如Oracle、MySQL和SQL Server。而且无论你是使用Java、.NET或任何其它的编程语言或框架。
在Oracle 里,如果你想编写存储过程你当然应该使用PL/SQL包。在这篇文章里,假设你一般了解PL/SQL 和非常熟悉PL/SQL 包。这篇文章关注于一个令人讨厌的错误,这个错误使许多使用PL/SQL以及使用API(例如JDBC)从应用层调用它的开发人员很苦恼。这个错误就是“ORA-04068: existing state of packages has been discarded”。这个错误是当Oracle认为你的包状态出于某种原因是无效的时候抛出的。在这篇文章里,我们将讨论:
“ORA-04068”错误是什么和它为什么发生,它会影响什么,以及建议的解决方法
下面我们将从定义“ORA-04068”错误开始。
注意: 在这篇文章的示例里使用的是Oracle 9.2.0.3,不过相同的概念在Oracle 10g 中应该也是适用的。
“ORA-04068”错误是什么和它为什么发生?
如果我们使用Oracle的oerr程序看看ORA-04068的定义,我们会得到下面的信息:
$oerr ora 04068
04068, 00000, "existing state of packages%s%s%s has been discarded"
// *Cause: One of errors 4060 - 4067 when attempt to execute a stored procedure.
// *Action: Try again after proper re-initialization of any
// application's state. |
这个错误显示执行包的现有状态被另一个会话的一个动作无效化了。这个“状态”涉及包在规范或体中定义的任何全局变量(包括常量)。引起这个错误的动作一般是(但不局限于此)在得到了发生错误的会话所使用的连接之后包的重新编译。Oracle 建议的动作是重新初始化应用程序状态以调整包的新状态后重新尝试。
如果我们看些例子就会明白得多。
假设有下面定义的表:
create table t (x number );
有个叫做pkg 的包具有一个叫做p的存储过程,如下所示:
create or replace package pkg as
procedure p;
end pkg;
/ |
下面所显示的包pkg的包体定义了存储过程p只是插入一个常量1到我们先前定义的表t中去。
create or replace package body pkg as
procedure p
is
begin
insert into t(x) values (1);
end p;
end pkg;
/
注意在包规范或包体中没有全局变量或常量。换句话说,这个包是“无状态的”。
我们将使用两个SQL*Plus 会话来解释这个概念。在每一个“体验”中,我们会在每一个会话中编译包体之后执行存储过程pkg.p。现在开始体验1,在体验1中,即使我们在另一个会话中编译包体也不会出现ORA-04068错误的。这是因为这个包是“无状态的”,它在规范或体中没有定义任何全局变量或常量。
体验1
假设表t和包pkg的规范以及包体已经在包里定义了。在SQL*Plus 会话1中,我们执行包并获得下面的结果(这个包执行成功)。
注意:你可能注意到在这篇文章里启动SQL*Plus 有时和常规启动("SQL >")不一样——例如,在下面代码的“session 1”中。例如这可以使用命令“set sqlprompt 'session 1”来实现。
session 1> exec pkg.p
PL/SQL procedure successfully completed. |
在SQL*Plus会话2中,我们通过像下面这样重新创建包来重新编译它:
session 2> create or replace package body pkg as
2 procedure p
3 is
4 begin
5 insert into t(x) values (1);
6 end p;
7 end pkg;
8 /
Package body created.
session 2> show errors;
No errors. |
现在如果你回到会话1并重新执行包存储过程p,它会成功执行。
session 1> exec pkg.p
PL/SQL procedure successfully completed. |
让我们看看到目前为止我们所做的。我们定义了一个简单的包,只具有一个插入一个常量到一个数据表中的存储过程。我们开启了一个会话并执行这个包存储过程。在另一个会话中我们重新编译这个包(通过重新创建它)。当我们在第一个会话中重新执行这个包时,它运行正常——特别是,在会话2中包的重新编译在会话1中存储过程的第二次执行没有出现任何错误。
现在让我们重复这整个过程,只改变一个地方——添加一个全局变量到包体中(添加到规范中是一样的)。这意味着我们给包添加了“状态”。我们在下一节“体验2”中将讲述只做了这一个改变的相同体验。
体验2
我们从之前的会话退出。开启一个新的会话并在会话1中编辑我们新的包体,如下所示——注意在包的开始部分有一个常量声明,如下面的粗体显示。这是包的状态。这个常量不会被使用,但是它是这个体验没有得到结果的原因。
session1>@pkg_body
session1>create or replace package body pkg as
2 g_constant constant number := 1;
3 procedure p
4 is
5 begin
6 insert into t(x) values (1);
7 end p;
8 end pkg;
9 /
Package body created.
session1>show errors;
No errors. |
现在我们在会话1中执行存储过程p。
session 1> exec pkg.p
PL/SQL procedure successfully completed. |
开启一个新的会话“session 2”并通过重新创建这个包来重新编译它。
session 2> @pkg_body
session 2> create or replace package body pkg as
2 g_constant constant number := 1;
3 procedure p
4 is
5 begin
6 insert into t(x) values (1);
7 end p;
8 end pkg;
9 /
Package body created.
session 2> show errors; |
再次在会话1中执行存储过程p,得到下面的结果:
session1>exec pkg.p
BEGIN pkg.p; END;
*
ERROR at line 1:
ORA-04068: existing state of packages has been discarded
ORA-04061: existing state of package body "ORA92.PKG" has been invalidated
ORA-04065: not executed, altered or dropped package body "ORA92.PKG"
ORA-06508: PL/SQL: could not find program unit being called
ORA-06512: at line 1 |
发生了什么?当我们重新在会话2中编译包体时,我们重置了包的状态。换句话说,对于任何在包编译之前连接的会话,包的执行状态(在这种情况下,由在包体里指定给常量的值来定义)被从内存中删除了。注意,我们实际上没有改变这个状态(我们在重新编译的时候保持相同的常量值),但是Oracle没有跟踪这一级别上的细节。只要连接了Oracle,在会话2里,重新编译了包pkg——这个包的状态现在重置为一个新的状态——所以对于任何在这个重新编译发生之前已经连接到Oracle的已有会话来说,包的状态变为无效。因此任何存储过程或包的函数下一次执行时,就会立即抛出ORA-04068错误。
如果我们在第一次尝试中得到了ORA-04068错误,那么我们在会话1中重新执行这个包会发生什么呢?让我们看一下。
session 1> exec pkg.p
PL/SQL procedure successfully completed. |
如同你所看到的,第二次执行显示调用应用程序(在这个例子中是SQL*Plus)调整为新的状态(自从Oracle通知了一次改变的状态)并重新执行有了新状态的包。这是Oracle 所建议的动作(查看这一节的开始部分):
在恰当地重新初始化任何应用程序状态之后重新尝试。
下面讲述ORA-04068错误的一些影响。
“ORA-04068”错误的影响
要测量ORA-04068的影响,你所要做的一切就是google它。它的两个主要影响如下:
大多数企业应用程序使用缓存连接的连接池。现在无论何时部署一个新的包规范,都需要在生产过程中重新编译。你编译的时候,对于连接池中的所有连接,这个包的状态都会被无效化,因为这个包在获得连接之后进行了重新编译(作为连接池初始化的一部分,有时更早)。注意,无论你是否改变这个状态,无论你是否改变代码,都会如此。当一个存储过程或一个函数第一次被调用时,它将会失败并抛出“ORA-04058”错误。所以一般情况下,你需要记住要“刷出”连接池(意味着丢弃现有连接并获得到Oracle的新连接)。这通常导致应用程序部署的一个停机。例如,如果你正在使用tomcat 和在tomcat 中的一个连接池,那么你可能需要停止tomcat 并重启——以便它重新初始化连接池。那么如果有一个长时间运行的批处理正在使用一个连接来执行与需要重新编译的包完全无关的一些逻辑呢?那么你或者需要等待这个批处理执行完毕或者在部署过程中将它关闭以便你可以重新初始化连接池。正如你可能想到的,这在应用程序有效性方面会是个梦魇。
很糟糕的一个影响是开发人员会困惑于为什么一个简单包(具有一个状态)的重新编译会导致在Oracle中得到这个错误。特别是在其它的数据库例如SQL Server和MySQL没有相同的包概念,因此没有与存储过程或函数相关联的一个状态。所以,在这些数据库中,你可以重新部署存储过程,并且应用程序会透明地使用它们。对于其它的数据库这是否是一个正确的选择是具有争议的,并且不属于本篇文章要讨论的范围。除了了解ORA-04068错误的根本原因以及怎样处理它之外,这个错误还会使得开发人员放弃去一起使用存储过程(并从而放弃了使用存储过程所带来的所有好处),并在他们的应用程序代码中嵌入SQL语句(例如在Java代码中嵌入SQL)。
那么解决方法是什么?
在这一节,我们将讨论处理“ORA-04068”错误的许多解决方法。每一个解决方法都在它的可用性方面具有一些局限性。这些解决方法还显示了思考的过程,使得更容易理解推荐的解决方法和替代方法。
让我们从解决方法1开始。
解决方法1:使用无状态包
最简单的解决方法是在你的系统中只使用无状态的包。正如我们在前面章句提到的,当你重新执行一个无状态的包时即使是它在另一个会话中重新编译之后也不会发生ORA-04068错误的。这是因为没有可以被Oracle无效化的状态。
这个解决方法,虽然在理论方面很简单,但是具有以下明显的缺陷:
它使你不能定义任何状态,这导致代码很差。一般有两种类型的状态:
一个全局变量:一般应该尽量避免全局变量。我遇到过的确需要在一个PL/SQL包或体中定义全局变量的合理需求。
一个全局常量:几乎所有的重要产品系统都需要定义常量。如果你决定在你的系统中不允许定义常量,那么就会导致很差的代码、多次重复定义相同的值,当需求改变的时候就会影响系统中不只一个地方,因此降低了可维护性。
如果你已经有一个包含了定义状态的包的系统,那么这个解决方法会使得进行大量的重写。在这种情况下,你需要决定是否值得这么做。
让我们看下一个解决方法:
解决方法2: 将所有的包状态移到另一个包里
这个解决方法的思想是将包体或包规范中的所有包状态移到另一个包里,这个包作为“同伴状态包”。这意味着我们降低了需要处理“ORA-06068”错误的次数,因为这些包本身并不存储任何状态,尽管它们因为各自的状态而依赖于同伴包。在我的经历中,在包体执行中发生的大多数改变——如果我们执行这个解决方法那就不会导致ORA-06068错误。如果我们重新编译同伴状态包,那仍然会发生ORA-06068错误。
让我们看看这个解决方法的工作情况。
我们创建一个新的包叫做const ,如下所示,我们将我们之前定义的常量移到我们的包pkg的包体中。
create or replace package const as
g_constant constant number := 1;
end const;
/
show errors; |
包pkg的包规范没有改变,并且为了你的方便,下面重复一下:
create or replace package pkg as
procedure p;
end pkg;
/
show errors; |
这个包体改变了,以便它之中不再有常量定义(它移到了包const中),而且现在插入语句使用包const 中定义的常量以获得这个值。因此包pkg依赖于包const以获得由常量g_constant定义的它的状态:
create or replace package body pkg as
procedure p
is
begin
insert into t(x) values (const.g_constant);
end p;
end pkg;
/
show errors; |
假设我们改变了对包pkg的包规范并在我们的系统中安装了一个新的包const。现在我们登录到我们的会话1中并执行这个存储过程——它如意料般地执行成功:
session 1>exec pkg.p
PL/SQL procedure successfully completed. |
我们登录到会话2中并重新编译包pkg的这个包规范和包体:
session 2>@pkg_spec
session 2>create or replace package pkg as
2 procedure p;
3 end pkg;
4 /
Package created.
session 2>show errors;
No errors.
session 2>@pkg_body
session 2>create or replace package body pkg as
2 procedure p
3 is
4 begin
5 insert into t(x) values (const.g_constant);
6 end p;
7 end pkg;
8 /
Package body created.
session 2>show errors;
No errors. |
在会话1中,当我们重新执行这个存储过程时,尽管我们重新编译了这个包规定和包体,它仍然执行成功。这是因为这个包状态是在包const中的(它已经被重新编译了),并因此当我们重新编译包pkg时包状态没有改变。.
当我们如下在会话2中重新编译包const时会发生什么呢?:
session 2>@const
session 2>create or replace package const as
2 g_constant constant number := 1;
3 end const;
4 /
Package created.
session 2>show errors;
No errors. |
如果我们在会话1中重新执行包pkg,我们将如意料般得到ORA-04068错误。这个错误清楚地表明在包const中的包状态改变了,并因此使得依赖于它的包pkg被无效化。
session 1>exec pkg.p
BEGIN pkg.p; END;
*
ERROR at line 1:
ORA-04068: existing state of packages has been discarded
ORA-04061: existing state of package "ORA92.CONST" has been invalidated
ORA-04065: not executed, altered or dropped package "ORA92.CONST"
ORA-06508: PL/SQL: could not find program unit being called
ORA-06512: at "ORA92.PKG", line 5
ORA-06512: at line 1 |
当然,如果我们之后在会话1中重新执行这个包,它看起来如预期般执行成功:
session 1>exec pkg.p
PL/SQL procedure successfully completed.
解决方法2,尽管比解决方法1好些,但是具有以下缺陷:
它要求你总要将包的状态移到包外,因此使得包状态对于系统中的其它所有的包来说都是可见的。换句话说,你不能创建包私有的变量(或常量)(如果你在包体中声明一个变量或常量,它不能被其它的包访问到——它是定义它的包所私有的——这使得更好地封装代码)。这削弱了系统的封装性,从而降低了系统的可维护性。事实上,如果我们这么做,我们应该只将常量作为任何包状态的一部分(它是合理的并欢迎自我约束的)。
它要求你将包的所有状态移到一个同伴状态包里。这导致系统中同伴包的增大,所以这个解决方法不太好。如果你决定只有一个包具有所有其它包的状态,那么你将遇到另一个问题——在中央包里,一个变量或常量的改变会导致系统中所有其它包无效——甚至包括那些与这个变量或常量无关的包。只有你能决定这两个选择(中央状态包或每个包的同伴状态包)哪个适合于你。
如果你已有系统具有定义了状态的包,那么这个解决方法可能很难执行,因为它可能导致大量的重写。这种情况下你需要衡量是每一次部署出现连续的ORA-04068错误,还是要一次性重写系统。
我们下一组的解决方法是针对于前面提到的两个解决方法的改进,但它们有较大的缺陷,以至于在这篇文章里使得解决方法1或解决方法2是最终的推荐解决方法。但是,我强烈建议你看看下面两个解决方法来了解它们的机制,并基于你对系统的了解来作出判断。
解决方法3: 监测ORA-0408错误并重新执行包的存储过程
这个解决方法将处理错误的责任放到了客户端。它的思想是Oracle 通过生成错误ORA-04068给客户端提供了关于包状态已经被无效化的信息和由客户端来监测这个错误以及作出反应。客户端可以选择重新执行这个存储过程,如果它需要的话。我们已经看到这个解决方法看起来是工作在SQL*Plus 中,当存储过程的执行是在这个错误如意料般的发生之后。我们现在将看看在使用JDBC的Java程序中它的执行以及看看它是否管用。
首先让我们回到在包pkg中有状态的旧代码。所以我们在包体中重新引进状态,像以前一样——这个代码复制到下面以方便你查看:
create or replace package body pkg as
g_constant constant number := 1;
procedure p
is
begin
insert into t(x) values (1);
end p;
end pkg;
/
show errors; |
假设我们重新编译了包体以便我们具有恰当的新代码。我们将首先在一个Java程序中使用JDBC进行模拟一个会导致ORA-04068错误的环境。为此我们将:
使用JDBC在Java程序中获得一个连接,
在Java程序中使用JDBC执行pkg.p 存储过程,
在Java程序中休眠一段时间(10到20秒),
当我们的Java 程序休眠时,我们在一个单独的SQL*Plus会话中重新编译包pkg的包体,
在Java程序中使用JDBC重新执行pkg.p 存储过程——这将导致ORA-04068错误。
叫做ExecutePackageProcedureTwice 的Java程序显示如下。它执行了pkg.p存储过程,休眠了20秒以给我们充足的时间来重新编译这个包以模拟部署,然后重新执行这个存储过程:
package dbj2ee.article2.design1;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import oracle.jdbc.OracleDriver;
public class ExecutePackageProcedureTwice {
public static void main(String[] args) throws Exception {
Connection conn = null;
CallableStatement cstmt = null;
long sleepInSecs = 20;
try {
conn = getConnection();
cstmt = conn.prepareCall("{call pkg.p()}");
executePkg(conn, cstmt);
System.out.println("Sleeping for " + sleepInSecs + " seconds...");
Thread.sleep(sleepInSecs*1000);
System.out.println("Out of sleep...");
executePkg(conn, cstmt);
} finally {
try {
if(cstmt != null)
cstmt.close();
if(conn != null)
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private static Connection getConnection() throws Exception {
DriverManager.registerDriver(new OracleDriver());
return DriverManager.getConnection("jdbc:oracle:thin:@hercdev:1521:hercdev", "hercules", "hercules");
}
private static void executePkg(Connection conn, CallableStatement cstmt) throws Exception {
System.out.println("Executing the package...");
cstmt.executeUpdate();
conn.commit();
}
} |
现在让我们再生ORA-04068错误。
设置恰当的CLASSPATH路径,指向类的根路径和classes12.jar(这个Jar包含Oracle JDBC执行),执行这个类,我们得到下面的结果:
M:\articles\dbj2ee\articles>java -cp "M:\classes12.jar;.;M:\learning\java\dbj2ee
\build\classes" dbj2ee.article2.design1.ExecutePackageProcedureTwice
Executing the package...
Sleeping for 20 seconds...
当Java程序运行到它开始休眠的地方时,我们在一个单独的SQL*Plus会话中重新编译这个包:
SQL> @pkg_body
SQL> create or replace package body pkg as
2 g_constant constant number := 1;
3 procedure p
4 is
5 begin
6 insert into t(x) values (1);
7 end p;
8 end pkg;
9 /
Package body created.
SQL> show errors;
No errors. |
然后在Java程序结束休眠后,它如预期般地在试图第二次执行这个包的时候丢出ORA-04068错误:
M:\articles\dbj2ee\articles>java -cp "M:\classes12.jar;.;M:\learning\java\dbj2ee
\build\classes" dbj2ee.article2.design1.ExecutePackageProcedureTwice
Executing the package...
Sleeping for 20 seconds...
Out of sleep...
Executing the package...
Exception in thread "main" java.sql.SQLException: ORA-04068: existing state of p
ackages has been discarded
ORA-04061: existing state of package body "ORA92.PKG" has been invalidated
ORA-04065: not executed, altered or dropped package body "ORA92.PKG"
ORA-06508: PL/SQL: could not find program unit being called
ORA-06512: at line 1
at oracle.jdbc.dbaccess.DBError.throwSqlException(DBError.java:134)
at oracle.jdbc.ttc7.TTIoer.processError(TTIoer.java:289)
at oracle.jdbc.ttc7.Oall7.receive(Oall7.java:573)
at oracle.jdbc.ttc7.TTC7Protocol.doOall7(TTC7Protocol.java:1891)
at oracle.jdbc.ttc7.TTC7Protocol.executeFetch(TTC7Protocol.java:955)
at oracle.jdbc.driver.OracleStatement.executeNonQuery(OracleStatement.ja
va:2053)
at oracle.jdbc.driver.OracleStatement.doExecuteOther(OracleStatement.jav
a:1940)
at oracle.jdbc.driver.OracleStatement.doExecuteWithTimeout(OracleStateme
nt.java:2709)
at oracle.jdbc.driver.OraclePreparedStatement.executeUpdate(OraclePrepar
edStatement.java:589)
at dbj2ee.article2.design1.ExecutePackageProcedureTwice.executePkg(Execu
tePackageProcedureTwice.java:38)
at dbj2ee.article2.design1.ExecutePackageProcedureTwice.main(ExecutePack
ageProcedureTwice.java:20) |
现在,正如我们所说的,我们知道在客户端级别(在这个例子中是在Java程序中)通过丢出的异常我们可以监测错误的代码并通过重新执行这个包来作出响应。最简单的执行如修改过的程序dbj2ee.article2.design2.ExecutePackageProcedureTwice 所显示——与第一个版本的不同之处用粗体显示,便于你查看。正如你所看到的,我们捕捉SQLException 并查看这个错误是否是ORA-04068——如果是,那我们重新执行这个包,否则我们再次抛出这个错误。
package dbj2ee.article2.design2;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import oracle.jdbc.OracleDriver;
public class ExecutePackageProcedureTwice {
public static void main(String[] args) throws Exception {
Connection conn = null;
CallableStatement cstmt = null;
long sleepInSecs = 20;
try {
conn = getConnection();
cstmt = conn.prepareCall("{call pkg.p()}");
executePkg(conn, cstmt);
System.out.println("Sleeping for " + sleepInSecs + " seconds...");
Thread.sleep(sleepInSecs*1000);
System.out.println("Out of sleep...");
executePkg(conn, cstmt);
} catch (SQLException e) {
if(reExecutionRequired(e)){
System.out.println("ORA-04068 detected - re-executing the package...");
executePkg(conn, cstmt);
} else
throw e;
} finally {
try {
if(cstmt != null)
cstmt.close();
if(conn != null)
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private static boolean reExecutionRequired(SQLException e) {
return "72000".equals(e.getSQLState()) && e.getErrorCode() == 4068;
}
private static Connection getConnection() throws Exception {
DriverManager.registerDriver(new OracleDriver());
return DriverManager.getConnection(
"jdbc:oracle:thin:@devhost:1521:ora92", "rmenon", "rmenon");
}
private static void executePkg(Connection conn, CallableStatement cstmt)
throws Exception {
System.out.println("Executing the package...");
cstmt.executeUpdate();
conn.commit();
}
} |
让我们看看当我们执行这个程序并在另一个会话中编译这个包的时候发生了什么。与前面一样,我们执行这个程序并得到下面的输出:
M:\articles\dbj2ee\articles>java -cp "M:\classes12.jar;.;M:\learning\java\dbj2ee
\build\classes" dbj2ee.article2.design2.ExecutePackageProcedureTwice
Executing the package...
Sleeping for 20 seconds... |
在另一个会话里,我们重新编译这个包:
SQL> @pkg_body
SQL> create or replace package body pkg as
2 g_constant constant number := 1;
3 procedure p
4 is
5 begin
6 insert into t(x) values (1);
7 end p;
8 end pkg;
9 /
Package body created.
SQL> show errors;
No errors.
SQL> |
当我们回到我们的Java程序时,它输出下面的信息作为重新执行这个包的一部分:
M:\articles\dbj2ee\articles>java -cp "M:\classes12.jar;.;M:\learning\java\dbj2ee
\build\classes" dbj2ee.article2.design2.ExecutePackageProcedureTwice
Executing the package...
Sleeping for 20 seconds...
Out of sleep...
Executing the package...
ORA-04068 detected - re-executing the package...
Executing the package... |
如你所看到的,我们监测这个错误并成功地重新执行这个包。
尽管这个解决方法看起来很好,但是它的缺陷很明显:为了执行它,我们将在我们的Java程序中每次调用一个存储过程时都需要捕捉这个异常。这个更改在大多数系统中都是被禁止的。这个解决方法还有另一个缺陷,我将在后面提到,这个缺陷使这个解决方法不能用于许多系统。
我们下一个潜在的解决方法精简了这一节中展示的解决方法,使得包的重新执行对于一个已有的系统是透明的,因此使得它在这个系统中的执行是切实可行的。
解决方法4:透明地监测ORA-0408错误并重新执行包的存储过程
这个解决方法的思想是:
我们用我们自己的封装类,叫做MyConnectionWrapper,来替代Oracle的连接执行。做这个替代的最好地方是在驱动级别——通过编写一个封装驱动——尽管你可以在连接池执行级别做这个替代(例如在数据源中)。
我们的连接封装会返回一个叫做CallableStatement的CallableStatement封装,而不是无论何时我们调用它之上的方法prepareCall()就执行Oracle的CallableStatement。在其它所有的方法中,这个封装类会用这个动作代替封装的连接,因此它的行为和一个普通连接对象方式一样。
我们的CallableStatement 封装会在调用它上面的“execute”方法时捕捉异常——如果它监测到ORA-04068错误,它会透明地对这个封装的CallableStatement对象重新执行这个方法。在其它所有的方法中,它会简单地以封装的CallableStatement 对象来代表它。
首先我们将执行我们自己的驱动,这个驱动执行java.sql.Driver接口并封装Oracle驱动类。MyDriverWrapper类显示如下:
package dbj2ee.article2.design3;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.util.Properties;
import oracle.jdbc.OracleDriver;
public final class MyDriverWrapper implements Driver {
private static final DriverPropertyInfo[] DRIVER_PROPERTY_INFO =
new DriverPropertyInfo[0];
public static final String ACCEPTABLE_URL_PREFIX = "jdbc:dbj2ee:orawrapper:";
private static Driver driver = new OracleDriver();
static {
try {
DriverManager.registerDriver(new MyDriverWrapper());
} catch (Exception e) {
e.printStackTrace();
}
}
public Connection connect(String url, Properties info) throws SQLException {
String myUrl = url.replaceFirst(ACCEPTABLE_URL_PREFIX, "jdbc:oracle:thin:");
System.out.println("new url: " + myUrl);
return new MyConnectionWrapper(driver.connect(myUrl, info));
}
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info)
throws SQLException {
return DRIVER_PROPERTY_INFO;
}
public boolean jdbcCompliant() {
return true;
}
public boolean acceptsURL(String url) throws SQLException {
return url != null && url.startsWith(ACCEPTABLE_URL_PREFIX);
}
public int getMinorVersion() {
return 0;
}
public int getMajorVersion() {
return 1;
}
} |
注意这个类是怎样定义它自己的私有前缀的——你可以设定任意的值。它还存储了一个OracleDriver 对象的实例,它是做实际工作的。在连接方法中,驱动在URL中进行替代,它的具有Oracle thin driver前缀的私有前缀无缝地创建了一个适用于OracleDriver 的url。然后它通过指定OracleDriver实例获得了Oracle连接。然后它封装了这个连接和类MyConnectionWrapper(过会儿我们将看看这个类)并返回了MyConnectionWrapper对象。这就是我们透明替代我们自己的连接对象的方式。注意,你可以以多种方式来进行——例如,你可以在数据源级别替代这个连接,而不是在连接级别。
类MyConnectionWrapper 显示如下。观察下面关于这个类执行的信息:
它将一个连接对象作为构造器中的一个对象并将它存储在一个私有的实例变量中。
它封装了所有版本prepareCall()方法执行中的MyCallableStatement类的所有CallableStatement对象。
其它方法的执行简单地用封装连接中相应的方法来代表它们的动作。
package dbj2ee.article2.design3;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Savepoint;
import java.sql.Statement;
import java.util.Map;
public class MyConnectionWrapper implements Connection {
private Connection connection;
public MyConnectionWrapper(Connection connection) {
this.connection = connection;
}
public CallableStatement prepareCall(String sql) throws SQLException {
return new MyCallableStatementWrapper(connection.prepareCall(sql));
}
public CallableStatement prepareCall(String sql, int resultSetType,
int resultSetConcurrency) throws SQLException {
return connection.prepareCall(sql, resultSetType, resultSetConcurrency);
}
public CallableStatement prepareCall(String sql, int resultSetType,
int resultSetConcurrency, int resultSetHoldability) throws SQLException {
return connection.prepareCall(sql, resultSetType, resultSetConcurrency, resultSetHoldability);
}
public void clearWarnings() throws SQLException {
connection.clearWarnings();
}
// ....... all other methods are simple delegation to the connection
// instance variable and are not being shown to conserve space.
} |
对于CallableStatement封装类的执行,它的父接口PreparedStatement和Statement的执行是必要的。因此我们创建三个封装对象——MyStatementWrapper封装了Statement对象;MyPreparedStatementWrapper封装了PreparedStatement对象,而MyCallableStatementWrapper封装了CallableStatement对象。
MyStatementWrapper 类是一个简单的封装类,它封装了Statement对象,下面显示了它的一部分——这个代码很容易理解:
package dbj2ee.article2.design3;
import java.sql.Connection;
import java.sql.Statement;
import java.sql.SQLWarning;
import java.sql.SQLException;
import java.sql.ResultSet;
public class MyStatementWrapper implements Statement {
Statement statement;
public MyStatementWrapper(Statement statement) {
this.statement = statement;
}
public void addBatch(String sql) throws SQLException {
statement.addBatch(sql);
}
// ....... all other methods are simple delegation to the connection
// instance variable and are not being shown to conserve space.
} |
MyPreparedStatementWrapper类是一个简单封装类,它封装了PreparedStatement对象,下面显示了它的一部分——这个代码很容易理解:
package dbj2ee.article2.design3;
import java.net.URL;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.ResultSet;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.ResultSetMetaData;
import java.sql.Array;
import java.io.InputStream;
import java.math.BigDecimal;
import java.io.Reader;
import java.sql.Date;
import java.sql.ParameterMetaData;
import java.util.Calendar;
import java.sql.Ref;
import java.sql.Time;
import java.sql.Timestamp;
public class MyPreparedStatementWrapper extends MyStatementWrapper
implements PreparedStatement {
private PreparedStatement preparedStatement;
public MyPreparedStatementWrapper(PreparedStatement preparedStatement) {
super(preparedStatement);
this.preparedStatement = preparedStatement;
}
public ParameterMetaData getParameterMetaData() throws SQLException {
return preparedStatement.getParameterMetaData();
}
// ....... all other methods are simple delegation to the connection
//instance variable and are not being shown to conserve space.
} |
MyCallableStatementWrapper 类显示如下。观察下面关于这个类的执行信息:
它扩展了MyPreparedStatementWrapper类。
像其它的封装类一样,它存储一个CallableStatement对象作为它的实例变量的一部分。
对于所有执行存储过程的方法来说,如果它透明地检测到ORA-04068错误,它就会覆盖这个执行,重新调用这个方法。注意,事实上,你可能还需要以类似的方式覆盖其它的一些从PreparedStatement 继承而来的方法。
其它方法的执行简单地以封装对象CallableStatement中的相关方法来代表它们的动作。
package dbj2ee.article2.design3;
import java.io.InputStream;
import java.io.Reader;
import java.util.Map;
import java.sql.CallableStatement;
import java.sql.SQLException;
import java.sql.Blob;
import java.sql.Clob;
import java.sql.Array;
import java.math.BigDecimal;
import java.net.URL;
import java.sql.Date;
import java.util.Calendar;
import java.sql.Ref;
import java.sql.Time;
import java.sql.Timestamp;
public class MyCallableStatementWrapper extends MyPreparedStatementWrapper
implements CallableStatement {
private CallableStatement callableStatement;
public MyCallableStatementWrapper(CallableStatement statement) {
super(statement);
this.callableStatement = (CallableStatement)statement;
}
public boolean execute() throws SQLException {
boolean result = true;
try {
result = callableStatement.execute();
} catch (SQLException e) {
System.out.println("code:" + e.getErrorCode() + ", sql state: "
+ e.getSQLState());
if(reExecutionRequired(e)){
System.out.println("re-executing package ");
result = callableStatement.execute();
} else
throw e;
}
return result;
}
public int executeUpdate() throws SQLException {
int result = 0;
try {
result = callableStatement.executeUpdate();
} catch (SQLException e) {
System.out.println("code:" + e.getErrorCode() + ", sql state: " +
e.getSQLState());
if(reExecutionRequired(e)){
System.out.println("re-executing package ");
result = callableStatement.executeUpdate();
} else
throw e;
}
return result;
}
private boolean reExecutionRequired(SQLException e) {
return "72000".equals(e.getSQLState()) && e.getErrorCode() == 4068;
}
public URL getURL(int parameterIndex) throws SQLException {
return callableStatement.getURL(parameterIndex);
}
// ....... all other methods are simple delegation to the connection
// instance variable and are not being shown to conserve space.
} |
最后,我们可以看看我们的使用了这个解决方法的ExecutePackageProcedureTwice 类。它显示如下。它和这一节开头的ExecutePackageProcedureTwice 非常类似——除了以下不同(在类的清单中以粗体显示):
它打印出连接和可调用的声明类来显示这些类确实存在于我们的封装类中。
获得连接的代码首先确定我们的驱动类通过使用Class.forName()加载进来了。然后使用我们私有的前缀而不是“oracle:jdbc:thin:”前缀,以便当获取连接时我们的驱动会被DriverManager 选择到,从而使得所有相关的JDBC类都被替换为我们的封装类。
package dbj2ee.article2.design3;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
public class ExecutePackageProcedureTwice {
public static void main(String[] args) throws Exception {
Connection conn = null;
CallableStatement cstmt = null;
long sleepInSecs = 20;
try {
conn = getConnection();
System.out.println("connection class: " + conn.getClass());
cstmt = conn.prepareCall("{call pkg.p()}");
executePkg(conn, cstmt);
System.out.println("Sleeping for " + sleepInSecs + " seconds...");
Thread.sleep(sleepInSecs*1000);
System.out.println("Out of sleep...");
executePkg(conn, cstmt);
} finally {
try {
if(cstmt != null)
cstmt.close();
if(conn != null)
conn.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
private static Connection getConnection() throws Exception {
Class.forName("dbj2ee.article2.design3.MyDriverWrapper");
return DriverManager.getConnection(MyDriverWrapper.ACCEPTABLE_URL_PREFIX +
"rmenon/rmenon@devhost:1521:ora92");
}
private static void executePkg(Connection conn, CallableStatement cstmt)
throws Exception {
System.out.println("Executing the package...");
cstmt.executeUpdate();
conn.commit();
}
} |
当我们执行这个类的时候,我们得到下面的结果(注意连接类和可调用的声明类指向我们的封装类):
M:\articles\dbj2ee\articles>java -cp "M:\classes12.jar;.;M:\learning\java\dbj2ee
\build\classes" dbj2ee.article2.design3.ExecutePackageProcedureTwice
new url: jdbc:oracle:thin:rmenon/rmenon@devhost:1521:ora92
connection class: class dbj2ee.article2.design3.MyConnectionWrapper
callable statement class: class dbj2ee.article2.design3.MyCallableStatementWrapper
Executing the package...
Sleeping for 20 seconds... |
然后我在另一个会话中像以前一样重新编译这个包:
SQL> @pkg_body
SQL> create or replace package body pkg as
2 g_constant constant number := 1;
3 procedure p
4 is
5 begin
6 insert into t(x) values (1);
7 end p;
8 end pkg;
9 /
Package body created.
SQL> show errors;
No errors. |
当我们回头观察我们的Java执行时,我们会在程序成功地重新执行这个包后看到下面的内容:
M:\articles\dbj2ee\articles>java -cp "M:\classes12.jar;.;M:\learning\java\dbj2ee
\build\classes" dbj2ee.article2.design3.ExecutePackageProcedureTwice
new url: jdbc:oracle:thin:rmenon/rmenon@devhost:1521:ora92
connection class: class dbj2ee.article2.design3.MyConnectionWrapper
callable statement class: class dbj2ee.article2.design3.MyCallableStatementWrapper
Executing the package...
Sleeping for 20 seconds...
Out of sleep...
Executing the package...
code:4068, sql state: 72000
re-executing package |
注意,如果你使用一个连接池那么你可以指定连接池里正确的驱动使用相同的技术。
因此我们可以设计这样一个解决方法,它看起来对于几乎所有的情况都是透明的。我曾经以为这是最佳的解决方法。但是后来发现在这个看似最佳的解决方法中有些问题。现在我描述一下。
考虑下面的场景:
你有一个包pkg ,它依赖于包const 中的一个常量。包pkg有两个方法method1和method2,它们都依赖于常量const1——它的值设为1。
你从连接池得到一个连接。
在你的Java代码中,你执行方法pkg.method1——它使用常量的值,而这个常量的值现在是1。
现在,作为部署的一部分,编译包const ——常量的值变为了2。
你的事务执行下一步——调用方法pkg.method2。
因为你已经执行了在这一节中提到的“默默的重新执行技术”, method2 会默默地忽略ORA-04068并获得常量的新值,现在是2。
问题是这可能导致事务中结果不一致。这是因为你违反了这个假定——在包(或一般是包状态)中定义的常量应该在一个给定会话中始终保持为一个相同值——否则就不能保证你依靠事务的语法得到一致的结果。
因此这个解决方法在包的存储过程重新执行后不能给出正确结果的所有情况下是不可行的。这是可能发生的,比如举例来说,你的包的存储过程现在的执行依赖于先前包的状态。这种情况是比较常见的。
总结和推荐的解决方法
我们看了这篇文章中对于ORA-04068错误的多个解决方法,并对于每一个解决方法都做了大量的评测。下面是我基于各个场景做出的建议:
我建议不论在什么情况下,都尽可能地不要在包规范或包体中使用 全局变量。
最简单的解决方法是使用无状态的包(我们的解决方法1),如果可以这样,那么这就是我所推荐的。你应该努力使你的包无状态化。
第二个最佳解决方法(大多数情况下可行的方法)是为每一个包状态为独立的包添加同伴包。这确保了只有当你真的改变了同伴状态包的时候才会遇到ORA-04068错误——这种情况应该相对很少见——特别是当状态只包括常量的时候。如果你不想有同伴包,那么你可以有一个中央包包括系统中所有的常量——这会导致比一般更多的ORA-04048错误——但是注意,即使你只改变了一个包的状态,你也需要刷出你的连接池,所以这不像它听起来的那么差。
我不推荐解决方法4(或解决方法3),因为我发现很难担保它们可以在任何复杂的系统中起作用。然而它们很少会失败,这个解决方法就像一个定时炸弹一样准备好在这些很少见的环境中爆炸。