UML软件工程组织

将存储过程封装为EJB组件的方法
Cynthia M. Saracco Yesky 论坛

  集成 Web 应用服务器和数据库管理 (DBMS) 技术是很多新型商业应用的常见需求。在本文中,我们将讨论该集成的一个方面:如何在会话 Enterprise JavaBeans (EJB) 组件中设计与开发封装或调用现有 DBMS 存储过程的方法。您应该熟悉 EJB 技术、结构化查询语言 (SQL) 和 Java 数据库连接 (JDBC) 的基本知识,以便充分理解本文。

  如果您正致力于需要访问或修改在 DMBS 中数据的 Web 应用程序开发,那么可能已经在向基于 EJB 的设计转移。您可能会发现,通过使会话 EJB 组件利用 DBMS 存储过程,可以减少编码和维护工作,并可能提高数据访问性能。

  一些公司多年来一直在使用存储过程(stored procedure),很大程度上是因为它们可以帮助减少网络通信量,并提高分布式计算环境中的性能。通常,这些过程包含涉及多数据库操作的重要业务逻辑。远程应用程序调用这些过程,在 DMBS 服务器上执行它们所包含的 SQL 语句。当然,过程结束时,所有结果都返回给应用程序。

  这些旧有存储过程对 Web 应用通常是有用的。与其在 EJB 组件中复制这些逻辑,为什么不将这些过程作为方法封装在会话 bean 中呢?这样可以避免 DBMS 服务器和 EJB 组件中的冗余代码 -- 在考虑开发、调试和维护开销时,冗余代码将损耗开发效率。这还可能带来提高性能的好处。调用存储过程可以减少 EJB 组件原本不得不发出的 SQL 语句数量,从而减少与远程 DBMS 的通信开销。

  入门

  现在明白为什么要从会话 bean 调用存储过程了吧,下面我们看看如何开始。首先,需要使用适当的开发环境,该环境应该包括一个带有内置 EJB 支持的 Java 开发工具,一个 Web 应用服务器和一个关系 DBMS。我的参考配置包括 VisualAge for Java 企业版 3.0.2,WebSphere Application Server 高级版 3.0.2.1,以及 DB2 V7.1,所有这些都安装在一个 Windows NT 系统上。有关如何配置该环境以支持本文所述工作的详细信息,请参阅 "Leveraging DBMS Stored Procedures through Enterprise JavaBeans"(位于参考资料中)或参考产品手册。

  有了正确的软件环境,就可以开始了。虽然我们要讨论的编码模式可能适合于无状态会话(stateless session) bean,但它也可使用有状态会话(stateful session) bean 组件。但是,因为无状态会话 bean 比有状态会话 bean 消耗的系统资源更少,而且涉及的代码也更少,所以通常建议使用无状态会话 bean。

  首先要考虑的设计问题是如何在存储过程和 EJB 组件之间映射数据。存储过程可能需要多个输入、输出和输入/输出参数,并返回一个或多个结果集(代表数据行)。除非要对不同类型的过程使用不同的编码模式,您需要编写 EJB 组件以便处理所有这些可能性。

  处理输入(或者输入/输出)参数很简单:将存储过程需要的每个参数映射成 EJB 组件的输入参数。但是,处理存储过程的输出比较棘手。可能有多个输出参数和多个结果集要传回调用程序,需要将这些作为一个可序列化的对象返回,以符合 EJB 规范。可以编写自己的类,使其可以将这些数据打包成一个对象,并在该对象中包括所有必须的元数据。(该元数据将描述对象的内部结构,以便客户机知道如何处理。)但这需要大量工作。

  如果正在使用 VisualAge for Java 和 WebSphere,那么,有个更好的选项:使用它们的数据访问 Bean (DAB) 库。该库包含一些提供位于基本 JDBC 之上的函数层的类。可能会发现 com.ibm.db.CallableStatement 类特别方便,因为它允许创建一个可序列化的对象,该对象包含所有从存储过程返回的输出,包括多个结果集(如果有的话)和相关元数据。还有一个好处是,该库设计成可以支持任何支持 JDBC 的数据源,因此,它可以使 bean“与 DBMS 无关”。有了 DAB 库,就可以用一个编码模式在会话 EJB 组件中封装任何存储过程。甚至可以在 EJB 客户机中使用一个通用的编码模式,来处理任何从封装器方法返回的结果。

  回顾开发任务
  我们来讨论一下使用通用编码模式,来集成 EJB 组件和 DBMS 存储过程的步骤:
  确定要将哪个存储过程封装成 EJB 方法。如果该过程不存在,则遵循 DBMS 标准过程来创建和调试。
  确定要使用哪个无状态会话 EJB 组件。如果该 EJB 不存在,则遵循 Java 开发环境的标准过程来创建和调试。
  扩展 EJB 组件的远程接口,以包括用于封装存储过程的新方法。
  扩展 EJB 组件的实现,以包括封装存储过程的新方法的逻辑。连接到数据库、调用存储过程、处理所有结果集和处理所有异常是后面要解决的问题。
  通过构建一个客户机应用或 Servlet,来调用 EJB 组件封装器方法,以测试所做的工作。
  头两项是基本编程任务,您可能已经熟悉。根据所用产品的不同,个别步骤可能会略有不同,但是大多数产品都有工具来提供帮助。例如,如果正在使用 VisualAge for Java 和 DB2,则可以利用“存储过程构建器”来完成步骤 1,以及利用 EJB 开发特性来完成步骤 2。本文将不集中讲述头两步。但是,其余三步需要详细讲解。本文将在一个实际示例的环境下讨论这些步骤中的每一步。
  浏览应用方案
  假设需要构建一个应用,该应用支持一家公司的市场营销分部,该分部维护面向金融的 Web 站点。该站点允许人们注册为客户,跟踪他们的投资总额,以及在电子公告板上发表意见。另外还假设,支持该站点的数据存储在 DB2 的表中。以下代码样本显示如何创建这些表。
  在 DB2 中创建样本表的 SQL 语句
create table client (
id int not null primary key,
name varchar(30),
email varchar(30),
phone varchar(12),
regdate date,
mktg char,
constraint check1 check (mktg in (‘y‘, ‘Y‘, ‘n‘, ‘N‘))
)
create table portfolio (
id int not null,
clientID int not null references client,
ticker varchar(10) not null,
cost decimal (9,2),
qty int,
date date,
primary key (id, clientID, ticker)
)
create table boards (
msgno varchar(15) not null primary key,
subject varchar(40),
date date,
clientID int not null references client
)
  该数据库还包含一个特别重要的存储过程。过程 CLIENTREPORT 提供注册站点用户的综合概要,包括用户投资和他们在公告板上讨论的问题。这个报告还包括客户名称和电子邮件地址,以便在用户提出有关有潜在价值的附加产品或服务方面的建议时,市场营销人员可以与这些用户联络。要在会话 EJB 组件中封装的就是这个过程。
  因为此过程可能用多种语言(包括 Java 编程语言)编写,这里就不显示它的完整内容。不管怎么说,源代码确实不那么重要,因为您不能假设总是可获得它们。但是,为了告诉您存储过程有什么内容,这里显示了它包括的三个 SELECT 语句:
  CLIENTREPORT 存储过程中的 SQL 语句:
select name, e-mail from client where id = ?
select id, ticker, cost, qty, date from portfolio where clientid = ?
select msgno, subject, date from boards where clientid = ?
  问号表示,该语句将依赖运行时来自调用程序的输入,在这种情况下,调用程序必须提供一个代表感兴趣的客户标识的有效数据值。通过这些语句,您可以猜出,存储过程将需要一个输入参数(用于客户标识),返回两个输出参数(用于客户名称和电子邮件地址),并返回两个结果集(一个包含有关客户投资总额的数据,另一个包含有关客户公告板发表内容的数据)。
  修改 EJB 组件的远程接口
  现在我们来开始 EJB 组件代码工作。
  既然要使封装器方法对 EJB 组件客户机可用,我们需要扩展 bean 的远程接口。将使用一个名为 Analysis 的无状态会话,并包括一个 lookupClient 方法,以用于存储过程封装器。该方法需要一个整数作为输入,以代表要报告的客户标识,它返回一个 DAB CallableStatement 对象(位于 com.ibm.db.* 包中)。将把该过程返回的任何异常转换成 RemoteExceptions(这适用于与 EJB 1.0 兼容的会话 bean)。
  以下编码示例显示了 EJB 组件远程接口的修改部分。
  EJB 组件远程接口
// Enterprise JavaBean Remote Interface for Analysis session bean
public interface Analysis extends javax.ejb.EJBObject {
// remote interface for our lookupClient method
com.ibm.db.CallableStatement lookupClient(java.lang.Integer clientId)
throws java.rmi.RemoteException;
. . .
}
  请注意,如果使用 VisualAge EJB 组件向导,则无需对此进行手工编码。替代方法是,在 bean 的实现类中对此方法编码之后,通过菜单项来将该方法提升(promote)到 bean 的远程接口,然后,将自动添加必需的代码。
  编码存储过程封装器方法
  现在可以集中讲述 bean 实现类本身,将在该实现类中包括调用存储过程的代码,并将其所有输出作为 com.ibm.db.CallableStatement 对象返回。包含调用 CLIENTREPORT 存储过程的 lookupClient(...) 方法的完整实现。将在后续章节中详细讲解每个代码块(参考代码中的注释)的逻辑,以便您更好地理解如何为自己的存储过程实现类似的方法。
  连接到数据库
  让我们更详细地查看此代码的各部分。
  在调用存储过程之前,需要建立一个到 DBMS 的连接。有两种方法做得到:使用 1.0 样式的连接,或者使用 JDBC 2.0 样式的 DataSource。在 WebSphere 环境中,通常选用后者,因为它提供连接池(connection pooling),这可以更有效地使用系统资源。出于这种原因,我们的编码模式使用 DataSource。
  除了确定要建立的连接类型之外,还应该考虑要将连接逻辑放在 bean 中的什么地方。有多个选择:
  直接放在封装器方法(wrapper method)中
  放在私有辅助方法(helper method)中
  放在 ejbCreate() 方法中(并将相应的断开逻辑放在 ejbRemove() 方法中)
  这些方法的利弊超出了本文的范围。为简单起见,样本代码将所有连接/断开逻辑直接放在方法中。
  代码块 1 显示了在使用 VisualAge for Java 3.0.2 和 WebSphere 3.0.2.1 时,如何使用 DataSource 进行连接。我们创建了一个散列表,在其中填充适合于 WebSphere 环境的值,然后建立一个 InitialContext。代码的以下几行利用该初始上下文和 Java 命名和目录接口 (JNDI) 服务,来获得期望的 DataSource 的索引,我们以前在 WebSphere 中用“管理控制台”创建了该 DataSource。本例中的 DataSource 名为 LocalDB2Sample。下一步,使用该 DataSource 来获得一个连接,并向其传递合适的数据库用户标识和口令。从连接池获得连接之后,可以将该信息提供给 DAB DatabaseConnection 对象,来设置它所需的连接规范。最后,将 autoCommitMode 设置成 false,因为 EJB 组件负责处理事务管理服务。
  测试时,在 VisualAge for Java WebSphere 测试环境中运行使用 DataSource 的 EJB 组件会很方便。有关如何在产品发行版 3.0.2 中这样做的指示,请参阅 David Zimmerman 所著的 "Creating DataSources in the VisualAge for Java WebSphere Test Environment"(在参考资料中)。
  调用存储过程

  建立了连接之后,可以集中讲述如何调用存储过程了。如封装器方法编码示例中的代码块 2 所示,首先创建一个 DAB StatementMetaData 对象,该对象中有存储过程的规范。下一步,定义要执行的 SQL 语句。在这里将要调用 CLIENTREPORT 过程,该过程需要一个输入参数(用于客户标识)和两个输出参数(用于客户名称和电子邮件地址)。下一步,将参数添加到规范中。对于每个过程参数,都指定了参数名,其数据类型及其参数模式。

  代码块 3 创建即将执行的 DAB CallableStatement 对象。CallableStatement 代表可用来执行存储过程的 SQL。创建完对象之后,将其元数据设置成在代码块 2 中指定的形式。然后将 DatabaseConnection(在代码块 1 中创建)与该 CallableStatement 关联。

  下一个任务很简单:需要执行 CallableStatement 对象,这将使 DBMS 运行存储过程。但是,在这样做之前,必须通过 EJB 客户机应用程序,将过程的输入参数设置成传入方法的值。代码块 4 中显示了这种逻辑。

  检索存储过程的输出并返回到调用程序

  在封装器方法编码示例的代码块 5 中,将检索存储过程返回的输出参数。想起来了吗?这些参数代表 Web 站点客户的名称和电子邮件地址。但是,不需要显式地检索存储过程返回的结果集。(这些结果集包含有关客户投资总额和公告板发表信息的数据)。您可能要问:为什么会这样呢?

  某些 DBMS 要求,在获得任何输出参数值之前,要从存储过程返回的结果集检索所有需要的值。由于这种要求,在通过 getParameter() 方法进行特别请求之前,CallableStatement bean 不从数据库获得任何输出参数,因为何时从结果集检索数据是由用户控制的。缺省情况下,在执行存储过程之后,将自动检索结果集,并将其存储在高速缓存中。但是,必须显式检索输出参数,并将其存储在高速缓存。

  检索完输出参数之后,将 DAB CallableStatement 返回给 EJB 组件的调用程序。该对象现在包含过程返回的所有输出(包括结果集),和帮助调用程序正确分析对象语法的适当的元数据。当我们查看调用会话 bean 封装器方法的样本客户机应用程序时,将看到如何去做。

  如果您熟悉 JDBC,可能会问:为什么不在此代码块中显式发出 commit 语句。确实,如果使用的是 JDBC 1.0 样式的连接,可能需要(否则,当在 "finally" 块中关闭数据库连接时,将逆序恢复所做的工作)。但是,使用 DataSource 并接受 WebSphere 缺省的 EJB 组件属性 (TX_REQUIRED),WebSphere 将自动为我们的工作提供事务管理。因此,不再需要显式的 commit 语句。

  处理异常与关闭打开的资源

  当然,在执行会话 bean 时可能会出错。因此,需要提供异常处理。代码块 6 包括适合于与 EJB 1.0 兼容的 bean 的简单异常处理程序。它只是捕获遇到的任何异常,包括一个适当的错误消息,并将异常作为新的 RemoteException 抛回给调用程序。

  另外,该代码块还包含一个 "finally" 块,以确保关闭所有打开的资源。在这里,释放任何与 CallableStatement 对象关联的资源。下一步,除去在工作中所用的任何对连接的 DAB 引用。最后,确保关闭 WebSphere 连接。

  构建客户机应用程序

  构建了 EJB 封装器方法之后,该集中讲述客户机应用程序了。与 EJB 组件一样,首先展示客户机应用程序的完整代码样本。然后,将详细讲述个别代码块。

  这里显示的客户机应用程序 -- ClientAnalysis -- 使用 RMI/IIOP 与 EJB 组件通信。其工作很简单:创建会话 bean,调用它的 lookupClient(...) 方法,处理该方法返回的 DAB CallableStatement 对象,然后除去 bean。将该应用程序编写成处理 CallableStatement 的通用客户机,即,假设事先不知道有关 CallableStatement 内部结构的任何信息。相反,我们严格依赖其中包含的元数据,来分析对象的语法,并使用其相关组件,如过程返回的输出参数和结果集。这种方法演示了通用的编码模式,可以在处理 CallableStatement 的任何应用程序中使用。就这样,它补充了在无状态会话 EJB 组件中对封装存储过程所用的通用编码模式。

  创建 EJB 组件并调用其封装器方法

  客户机应用程序的代码块 1 以 main(...) 方法开始。它指定感兴趣的客户标识,并调用一个私有辅助方法,来获得正在使用的会话 EJB 组件。执行完 bean 之后,调用 lookupClient(...) 方法。这是封装 CLIENTREPORT 存储过程并返回 DAB CallableStatement 的方法。

  需要详细讲述私有辅助方法 -- createEJB()。因为 EJB 组件创建工作可能会根据所用的 Web 应用程序而略有不同,所以,选择将这个工作隔离成单独的方法。特别是,由于与该上下文相关的特定属性将会改变,所以,获得 JNDI InitialContext 的方法可能不同。

  该 createEJB() 方法创建一个散列表,然后用适合于软件环境的值填充。下一步,创建一个新的 InitialContext 对象,该对象用于通过 JNDI 服务获得对 EJB 组件的远程引用。因为从 JNDI 上下文返回 JNDI(这是在 IIOP 之上使用 RMI 的编码需求),所以,限制了该远程引用。获得 EJB 组件宿主之后,创建一个无状态会话 bean,然后将其返回给客户机应用程序的 main 方法。

  处理返回的对象

  客户机应用程序的代码块 2 处理 EJB 组件返回的 DAB CallableStatement 对象。首先定位与 CallableStatement 关联的根元数据对象。因为 CallableStatements 可以获得多个结果集,所以,多个 StatementMetaData 对象可以链接在一起,并包括在 CallableStatement 中。而链的根总包含描述 SQL 语句的元数据和相关参数,因此,这就是我们的开始之处。这允许我们获得 CallableStatement 中包括的参数数目。返回的数目将包括过程的所有 IN、INOUT 和 OUT 参数。通过使用循环,可以处理所有参数并打印每个参数的相关信息,包括参数名、相应的 Java 类和模式(指明 IN、INOUT 或 OUT 模式的数字)。

  下一步,查看结果集并处理它们。首先,确定 CallableStatement 对象中包括的结果集数目。通过使用循环,可以获得每个用 DAB SelectResult 对象表示的结果集。然后,使用另一个私有辅助方法 processRS(...) 来处理结果集。processRS(...) 方法确定传递给 SelectResult 并包含在其中的行和列的数目。假设有一些行存在,它使用嵌套循环来打印有关所有行中的所有列的信息。该信息包括列名和它的值。

  目前为止,客户机应用程序的工作几乎完成。代码块 3 除去会话 bean,打印一行表明已完成,然后终止。当然,在代码块 3 之后的代码处理任何遇到的异常。在这里,只打印一个堆栈跟踪。

  总结

  希望您已理解会话 EJB 组件如何利用封装在旧有 DBMS 存储过程中的商业逻辑。这样做的其它可能的好处包括:减少 EJB 服务器和 DBMS 之间的网络通信量,提高生产力,以及降低总体软件维护成本。如果遵循本文中列出的编码模式,则无论与过程相关的参数或结果集如何,您都可以将任何类型的存储过程作为方法封装在无状态会话 bean 中。而且,您将可以使用通用编码模式来调用任何这样的 EJB 组件,并处理它返回的对象,而不必事先知道该对象的内部结构。

版权所有:UML软件工程组织