用 VisualAge for Java 实现 EJB 的 OO 设计
还记得 EJB 规范版本 1.0 中描述的吗:VisualAge for Java Enterprise Update EJB Development Environment 支持两种类型 Enterprise JavaBean 的开发:
- 表示活动对象的会话 bean,它不是“属于”单客户机(“有状态”会话 bean),就是完全可计算的,没有持久状态,以及在多个客户机之间共享(“无状态”会话 bean)
- 表示共享商业对象的实体 bean,它以持久存储机制(如关系数据库)存储,其存活期超过一个用户会话。有两种实体 Bean 类型:“容器管理的”的 bean,其持久性由 EJB 服务器本身管理;以及“bean 管理的”bean,其持久性必须由 bean 开发人员提供的代码处理。
VisualAge for Java(扩展到 WebSphere)提供一种将实体 Bean 映射 DB2 的简单机制。这种简单性来源于以下假设:EJB 中的每个属性都映射到数据库某表中的某列。这种映射根据类型进行 -- Java 中的 String 映射成 SQL 中的 VARCHAR,int 映射成 INTEGER,以此类推。VisualAge for Java 还允许 bean 开发人员以某些简单方式定制这种映射。例如,可以将诸如 "name" 这样的 "Complex" 属性映射成两列:"firstname" 和 "lastname"。
但是,有一条映射规则却与众不同。对于不能转换成标准 SQL 类型的对象,将其映射成 BLOB(二进制巨对象),最大长度为 1 MB。这条规则适用于任何从 Java 中 Object 派生的类型 -- 也就是说,您可能创建的每个类。
对于在关系数据库中存储,这是一种简单的解决方案,但是对于大多数应用程序,并不是最佳方案。在关系数据库中表示对象数据的方法通常有两种。一种方法是前面提到的 "BLOB" 方法,其中将对象序列化成二进制形式,并存储在表的某一列中。这种方法有以下缺陷:
- 其它应用程序(如报表工具)无法读取 BLOB
- 无法通过 SQL 查询 BLOB,使数据挖掘和表维护难以进行
- 创建 BLOB 应用程序的后续版本可能无法读取 BLOB。这是个棘手的问题,只有深入了解 Java 序列化的工作原理并制定周密计划才能解决。
由于这些缺陷,开发人员通常将对象映射成关系数据库,以便在关系数据库模式中保持对象关系。有关该方法的解释,请参阅 Crossing Chasms: A Pattern Language for Object-RDBMS Integration。现在很快回顾一下此方法,来看看如何将它应用到 VisualAge for Java 中的 EJB。
![]() ![]() |
![]()
|
在最普遍映射方法中,对象关系由数据库中的外键关系表示。例如,假设有以下关系:

在 Java 中,用名为 Resume 的 Java 类表示这个关系,该类有一个类型为 Address 的实例变量。在运行期间,每个 Resume 实例都包含一个 Address 类的实例。如果仔细观察这个例子,就会发现还要表示另一种关系。Resume 可能还包含一工作历史,(在 Java 中),可能将其表示成 Job 的一个集合。因此,要表示以下关系:

(例如)可以向类型为 java.util.Vector 的 Resume 类中添加一个实例变量。在运行期间,Vector 将包含 Job 的实例。因此,可按如下定义一个非常简单的类:
import java.util.*; |
到目前为止,还没有什么新东西。我们都知道如何在 Java 中表示对象,否则我们就不是 Java 程序员了。问题在于,这种 Java 设计如何映射到关系数据库?以及那又怎样帮助我们理解如何在 EJB 中实现我们的设计?马上就有答案 -- 耐心一点,下一部分的讨论将回答所有问题。
让我们看一下同一个对象设计是如何在关系数据库中表示的。如 [Brown96] 所述,如前一例的一对一关系 (Resume 对 Address) 在关系数据库中由外键表示,外键从“所有者”表指向“被拥有者”表。下列表演示了这种方法:
![]() ![]() |
![]()
|
PrimaryKey | Name | AddressFK |
1000 | Bob Smith | 2013 |
![]() ![]() |
![]()
|
PrimaryKey | Street | City |
2013 | 203 Maple Ln. | Raleigh |
Resume 表中的 AddressFK 列中包含一个指向 Address 表的外键 -- 每个 Resume 行中都有一个实际上指向 Address 的指针。一对多关系同样用外键存储,但外键指向 相反方向。每一个“被包含”行都有一个外键指向“包含”它的行。下表演示此过程:
![]() ![]() |
![]()
|
PrimaryKey | ResumeFK | JobTitle |
1011 | 1000 | Senior Programmer |
1012 | 1000 | Programmer/Analyst |
在此示例中,显示的两个 Job 行都有一个外键指回 Resume 表,由于有了外键,两个 Job(1011 和 1012)可以指回到前面的主键为 1000 的 Resume。现在可以了解解决方案的基本轮廓了。如果要将 Java 中的对象模型映射到关系数据库,必须通过某种方式创建并重组这些外键关系。简而言之,这就是 EJB 中特殊用途代码要做的事。
![]() ![]() |
![]()
|
这样看来,需要的是两方面的最佳组合。我们不但要象在标准 Java 类中那样表示对象关系,而且还想利用 EJB 中自动持久性特性。下面看一下 Resume 示例是如何通过 EJB 实现的,就会知道怎样兼顾二者。最终将创建三个 EJB:一个表示 Resume,一个表示 Address,一个表示 Job。决窍在于:如何将这三个 EJB 结合在一起。
首先要看一下的是 Resume EJB 的远程接口。如下所示:
package com.ibm.ejbs.examples; |
关于这个接口,有两点要指出。第一,getAddress() 方法返回 Address 实例。第二,getJobs() 方法返回枚举值(在本示例中,返回 Jobs 的枚举值)。理解如何实现这两个方法,是理解这种 EJB 关系管理方法的关键所在。远程接口方法说明了: 可以 得到相关的 Address 和 Job 的枚举项,而不说明 如何 做到。“如何做”方法由外键处理,下面就将看到。但是,在这之前,先看一下如何创建这个特别的 EJB。
用 VisualAge for Java “创建 EJB” 智能向导生成这个 EJB。使用这个智能向导创建一个容器管理的实体 Bean "Resume"。智能向导自动创建一个 primaryKey 实例变量,并生成一个主键类 "ResumeKey"。然后使用“创建域”智能向导,并选中“生成读写方法”复选框,来添加 "name" 字段。利用它的读方法和写方法方法,使用同一个智能向导来创建另一个字段 "addressFK"。然后使用“添加到 - 远程接口”菜单,将 getName() 和 setName() 方法提升为 Bean 接口。最后,使用“创建方法”智能向导,添加 getAddress() 和 getJobs() 方法,并将它们添加到远程接口。
创建 Address 和 Job EJB 的方法大体相同。使用“添加 EJB”智能向导,创建一个 CMP 实体 EJB "Address" ,然后添加 street、city、state 和 zip 字段。然后,将这些字段的读方法和写方法提升为远程接口,并添加 ejbCreate() 方法,该方法允许立即设置所有属性。最后,创建带有 "jobTitle" 字段和 "resumeFK" 附加字段的 Job EJB。
我们已经看到如何创建 EJB,现在来看一下如何实现上面提到的 getAddress() 和 getJobs() 方法。首先看一下 getAddress() 代码,因为它最简单,并且不涉及太多额外 EJB 代码。
public Address getAddress() { |
现在预演一下这个方法,然后了解它的工作原理。代码中 "try" 块之前的开始部分只对 addressHome 和 initialContext 进行简单初始化。obtainInitialContext() 方法通过创建 InitialContext 的新实例,从一组标准 Properties 获得初始环境。这个代码是标准样本,这里不再讨论。obtainAddressHome() 也是标准代码 -- 它使用 initalContext 来获得对 AddressHome 的引用,AddressHome 存储在 addressHome 实例变量中。本示例唯一不寻常之处在于:这个代码位于 EJB 中,而不在 EJB 客户机中(象大多数 WebSphere 示例演示的那样)。这是关键所在 -- Resume EJB 既是客户机, 同时 也是服务器 -- 它将作为 Address 和 Job EJB 的客户机。
代码的下一部分(try 块中的代码)将外键索引转换成对象。其实很简单 -- 首先使用 Resume 中已有的地址外键值,创建 AddressKey 的一个新实例。然后用 AddressHome 实例查询那个键的 Address。这由 findByPrimaryKey() 方法完成,findByPrimaryKey() 方法在所有 EJB Home 中自动定义。然后,返回 Address 实例。
getJobs() 方法与之非常类似。现在看一下它的代码:
public java.util.Enumeration getJobs() { |
和前一个方法一样,代码的开始部分对 jobsHome 中的 InitialContext 和 JobHome 实例进行简单初始化。try 块中的代码更为有趣。通过向 JobHome 发送消息 findByPersonFK(),来获得这个方法所返回的枚举值。传给这个方法的自变量是 Resume 中的 primaryKey。该方法在 JobHome 中定义,如下所示:
public interface JobHome extends javax.ejb.EJBHome { |
还记得一对多关系的表示方法吗:由多行指回包含这些行的对象所在的那行。JobHome 通过执行以下 SQL 代码来实现该方法,在 JobBeanFinderHelper 类中定义:
public interface JobBeanFinderHelper { |
这段 SQL 代码返回一组行,这些行表示了有一个与作为参数传递的 Resume 主键对应的 ResumeFK 的那些 Job。这就是要与 Resume 关联那组对象,这样,我们的任务也就完成。
![]() ![]() |
![]()
|
虽然本文概述了 EJB 中关系管理解决方案的基础,但是并没有解决这种方法的所有问题。还有其它问题需要解决。例如,没有解决创建对象之间的关联问题,例如,将特定 Address 实例与 Resume 相关联。本文只讲述了问题的“读方法”部分,相应的“写方法”部分涉及到从 Address 实例中取出主键,然后在 Resume EJB 中设置外键。
另一个关键问题是:对于商业逻辑范畴之外的唯一键,如何管理它们的生成。可以将 Address 表中的所有列作为 Address 表的主键,但这样不是特别有效。或者,可以创建一组 Generator 类(同样也可以是 EJB),来处理每一个新 Address 实例中主键的生成。这个问题本身就很有趣,有可能成为 VisualAge for Java 中有关 EJB 的另一篇文章的好题目。