当前位置:Linux教程 - Linux - Bean管理持续化实例

Bean管理持续化实例

原著:Dale Green
译者前言
有关本章中相应源代码的下载,请参看一个Session Bean的示例

正文
数据是绝大多数商业应用程序的核心。在J2EE应用程序中,entity bean反映了存储在一个数据库中的商业对象。对于使用bean管理持续化的entity bean,你必须编写代码以访问数据库。尽管编写这样的代码会增加一些额外的工作量,但是与此同时,你对entity bean如何访问数据库也获得了更多的控制。
在这一章中,我们将讨论一个使用bean管理持续化的entity bean的编程问题。对于entity beans的相关概念,请参阅Entity Bean是什么?.

SavingsAccountEJB示例
在这一部分的这个entity bean表现了一个简单的银行帐户。SavingsAccountEJB的状态存储在一个关系型数据库的savingsaccount表中。savingsaccount表是用下面的SQL语句建立的:
CREATE TABLE savingsaccount
(id VARCHAR(3)
CONSTRAINT pk_savingsaccount PRIMARY KEY,
firstname VARCHAR(24),
lastname VARCHAR(24),
balance NUMERIC(10,2));
SavingsAccountEJB示例需要以下代码:
1、Entity bean类(SavingsAccountBean)
2、Home接口(SavingsAccountHome)
3、Remote接口(SavingsAccount)
此外,这个示例还用到以下类:
1、一个名为InsufficientBalanceException的功能类
2、一个名为SavingsAccountClient的客户端类
在j2eetutorial/examples/src/ejb/savingsaccount目录下有这个示例的源代码。要编译这个代码,到j2eetutorial/examples目录下并输入ant savingsaccount。在j2eetutorial/examples/ears下有SavingsAccountApp.ear文件的示例。

Entity Bean类
在示例程序中,entity bean类名为SavingsAccountBean。在你浏览它的代码时,请注意它满足了所有使用bean管理持续化的entity bean的必要条件。首先,它实现了以下几个方面:
1、EntityBean接口
2、零个或多个ejbCreate和ejbPostCreate方法
3、Finder方法
4、商业方法
5、Home方法
另外,一个使用bean管理持续化的entity bean类必须满足这些条件:
1、类定义为public。
2、类不能定义为abstract或final。
3、包含一个空的构造函数。
4、它不能实现finalize方法。

EntityBean接口
EntityBean接口继承自实现了Serializable接口的EnterpriseBean接口。EntityBean接口中声明了许多方法,例如ejbActivate和ejbLoad,这些方法你必须在你的entity bean类中加以实现。我们将在下面对这些方法作详细讨论。

ejbCreate方法
当客户端调用create方法时,EJB容器调用相应的ejbCreate方法。典型的情况是,一个entity bean中的ejbCreate方法执行以下任务:
1、将实体状态添加到数据库中
2、对实例变量进行初始化
3、返回主键
SavingsAccountBean的ejbCreate方法通过调用private类型的insertRow方法将实体状态添加到数据库中,这样做的结果是执行了一个INSERT语句。下面是ejbCreate方法的源代码:

public String ejbCreate(String id, String firstName,
String lastName, BigDecimal balance)
throws CreateException {

if (balance.signum() == -1) {
throw new CreateException
(""A negative initial balance is not allowed."");
}

try {
insertRow(id, firstName, lastName, balance);
} catch (Exception ex) {
throw new EJBException(""ejbCreate: "" +
ex.getMessage());
}

this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.balance = balance;

return id;
}

尽管SavingsAccountBean类只有一个ejbCreate方法,但是一个enterprise bean可以包含多个ejbCreate方法。示例请参见j2eetutorial/examples/src/ejb/cart目录下的CartEJB.java。
在为一个entity bean编写ejbCreate方法,必须遵守以下规则:
1、访问控制修饰必须是public。
2、返回类型必须是主键。
3、参数类型必须满足Java 2 RMI API。
4、方法修饰不能是final或static。
throws子句可以包含javax.ejb.CreateException的你的应用程序中所指定的其它例外。如果输入的参数无效,一个ejbCreate方法通常会抛出一个CreateException。如果因为已经存在另一个相同主键的实体,ejbCreate方法不能建立一个新的实体,它会抛出一个javax.ejb.DuplicateKeyException(CreateException的子类)。如果一个客户端接受到一个CreateException或者是一个DuplicateKeyException,它会认为实体未被建立。
可以通过一个对于J2EE服务器未知的应用程序将一个entity bean的状态直接插入到数据库中。例如,可以使用一个SQL脚本在savingsaccount表中添加一行。尽管对应于这一行的entity bean不是由一个ejbCreate方法创建的,但是客户端程序还是可以对这个bean进行定位。

ejbPostCreate方法
对于每一个ejbCreate方法,你必须在entity bean类中编写一个ejbPostCreate方法。EJB容器在调用ejbCreate方法后会立即调用ejbPostCreate方法。与ejbCreate方法不同,ejbPostCreate方法可以调用EntityContext接口中的getPrimaryKey方法和getEJBObject方法。有关getEJBObject方法的详细信息,请参看传递一个Enterprise Bean的对象索引。不过,你的ejbPostCreate方法常常会是一个空方法。
ejbPostCreate方法必须满足以下条件:
1、参数的数量和类型必须与相应的ejbCreate方法相匹配。
2、访问控制修饰必须是public。
3、方法修饰不能是final或static。
4、返回类型必须是void。
throws子句可以包含javax.ejb.CreateException和你的应用程序中所指定的例外。

ejbRemove方法
一个客户端可以通过调用remove方法删除一个entity bean。这个调用会导致EJB容器调用ejbRemove方法,该方法会从数据库中删除这个实体状态。在SavingsAccountBean类中,ejbRemove方法调用一个名为deleteRow的private方法,这样做的结果是执行了一个DELETE语句。ejbRemove方法的源码比较短:

public void ejbRemove() {
try {
deleteRow(id);
catch (Exception ex) {
throw new EJBException(""ejbRemove: "" +
ex.getMessage());
}
}

如果ejbRemove方法遇到一个系统问题,它会抛出javax.ejb.EJBException。如果遇到的是一个应用程序错误,它会抛出一个javax.ejb.RemoveException。有关系统例外和应用程序例外的比较。请参看处理例外。
直接使用数据库删除也可以删除一个entity bean。例如,如果一个SQL脚本删除了包含一具entity bean状态的行,相应的entity bean也会被删除。

ejbLoad方法和ejbStore方法
如果EJB容器需要将一个entity bean的实例变量与存储在数据库中的相应的值进行同步,它会调用ejbLoad方法和ejbStore方法。ejbLoad方法会根据数据库中的值刷新实例变量,而ejbStore方法会将实例变量的值写入到数据库中。客户端不能调用ejbLoad方法和ejbStore方法。
如果一个商业方法与一个事务关联,容器会在执行商业方法前调用ejbLoad。而在商业方法执行后,EJB容器会立即调用ejbStore。因为容器会调用ejbLoad和ejbStore,所以你不需要在你的商业方法中刷新和存储实例变量。SavingsAccountBean类依靠容器进行实例变量和数据库的同步。因此,SavingsAccountBean的商业方法必须与事务关联。
如果ejbLoad和ejbStore不能在底层数据库中定位一个实体,它们会抛出javax.ejb.NoSuchEntityException。这个例外是EJBException的一个子例。因为EJBException是RuntimeException的一个子类,所以你不需要在throws语句中包含它。如果NoSuchEntityException被抛出,EJB容器会在将其返回到客户端前将其包装到一个RemoteException中。
在SavingsAccountBean类中,ejbLoad调用了loadRow方法,这样做的结果是执行了一个SELECT语句并将得到的值重新指派给实例变量。ejbStore调用了storeRow方法,这样做的结果是通过一个UPDATE语句将实例变量存储到数据库中。下面是ejbLoad方法和ejbStore方法的源代码:

public void ejbLoad() {

try {
loadRow();
} catch (Exception ex) {
throw new EJBException(""ejbLoad: "" +
ex.getMessage());
}
}

public void ejbStore() {

try {
storeRow();
} catch (Exception ex) {
throw new EJBException(""ejbStore: "" +
ex.getMessage());
}
}

Finder方法
finder方法允许客户端定位一个entity bean。在SavingsAccountClient程序中,可以通过三个finder方法定位entity bean:

SavingsAccount jones = home.findByPrimaryKey(""836"");
...
Collection c = home.findByLastName(""Smith"");
...
Collection c = home.findInRange(20.00, 99.00);

对于每一个客户端可用的finder方法,entity bean类都必须实现一个相应的以ejbFind为前缀的方法。例如,在SavingsAccountBean类中,ejbFindByLastName方法是这样实现的:

public Collection ejbFindByLastName(String lastName)
throws FinderException {

Collection result;

try {
result = selectByLastName(lastName);
} catch (Exception ex) {
throw new EJBException(""ejbFindByLastName "" +
ex.getMessage());
}
return result;
}

你的应用程序指定的finder方法,例如ejbFindByLastName和ejbFindInRange是可选的--但是ejbFindByPrimaryKey方法是必须的。正如它的名字所暗示的,ejbFindByPrimaryKey方法通过接收一个主键作为参数以定位一个entity bean。在SavingsAccountBean类中,主键是变量id。下面是ejbFindByPrimaryKey方法的源代码:

public String ejbFindByPrimaryKey(String primaryKey)
throws FinderException {

boolean result;

try {
result = selectByPrimaryKey(primaryKey);
} catch (Exception ex) {
throw new EJBException(""ejbFindByPrimaryKey: "" +
ex.getMessage());
}

if (result) {
return primaryKey;
}
else {
throw new ObjectNotFoundException
(""Row for id "" + primaryKey + "" not found."");
}
}

ejbFindByPrimaryKey方法对你来说看上去可能有点奇怪,因为它同时使用了一个主键作为方法的参数和返回值。然而,请记信客户端不会直接调用ejbFindByPrimaryKey。只有EJB容器会调用ejbFindByPrimaryKey方法,客户端只是调用在home接口中定义的findByPrimaryKey方法。
下面概括了你在一个使用bean管理持续化的entity bean类中实现的finder方法所必须遵守的规则:
1、必须实现ejbFindByPrimaryKey方法。
2、finder方法必须以前缀ejbFind开始。
3、访问控制修饰必须是public。
4、方法修饰不能是final或static。
5、参数和返回值的类型必须满足Java 2 RMI API。(这个条件只适用于在远程home接口中定义的方法,而不适用于在本地home接口中定义的方法。)
6、返回值类型必须是主键或主键集。
throws子句可以包含javax.ejb.FinderException和你的应用程序中所指定的其它另外。当一个finder方法请求的实体不存在时,如果这个finder方法只返回一个主键,方法会抛出javax.ejb.ObjectNotFoundException(FinderException的一个子类);如果这个finder方法返回的是一个主键集,它不会抛出例外,而是返回一个空的结果集。

商业方法
商业方法包含了你想要在entity bean中封装的商业逻辑。通常,我们通过将商业逻辑与数据库访问分离以使用得商业方法不访问数据库。SavingsAccountBean类中包含以下商业方法:

public void debit(BigDecimal amount)
throws InsufficientBalanceException {

if (balance.compareTo(amount) == -1) {
throw new InsufficientBalanceException();
}
balance = balance.subtract(amount);
}

public void credit(BigDecimal amount) {

balance = balance.add(amount);
}

public String getFirstName() {

return firstName;
}

public String getLastName() {

return lastName;
}

public BigDecimal getBalance() {

return balance;
}

SavingsAccountClient程序调用该商业方法的过程如下:

BigDecimal zeroAmount = new BigDecimal(""0.00"");
SavingsAccount duke = home.create(""123"", ""Duke"", ""Earl"",
zeroAmount);
. . .
duke.credit(new BigDecimal(""88.50""));
duke.debit(new BigDecimal(""20.25""));
BigDecimal balance = duke.getBalance();

对于session bean和entity bean,其中的商业方法所必须遵守的条件都是一样的:
1、方法名必须不能与EJB体系结构所定义的方法名冲突。例如,你不能将一个商业方法命名为ejbCreate或ejbActivate。
2、访问控制修饰必须是public。
3、访问修饰不能是final或static。
4、参数和返回值的类型必须满足Java 2 RMI API。这个条件只适用于在远程home接口中定义的方法,而不适用于在本地home接口中定义的方法。
throws子句可以包含你在你的应用程序中定义的例外。例如,在debit方法中,抛出了InsufficientBalanceException。对于系统级的问题,商业方法会抛出javax.ejb.EJBException。

Home方法
一个home方法包含了适用于所有属于特定类的entity bean的商业逻辑。与此相反,商业方法中的逻辑只适用于单个entity bean、具有唯一序列号的单个实例。在调用一个home方法的过程中,实例既不具有唯一的序列号也不具有反映一个商业对象的状态。因此,一个home方法不能访问bean的持续化状态(实例变量)。(对于容器管理持续化,一个home方法也不能访问关联关系。)
典型的情况是,一个home方法定位一个bean的实例集,并对其中的每一个实例应用商业方法。在 SavingsAccountBean类中的ejbHomeChargeForLowBalance采用了这种方法。ejbHomeChargeForLowBalance方法实现了一个向所有余额小于指定值的帐户收费的服务。这个方法通过调用findInRange方法定位这些帐户。在它遍历SavingsAccount实例集时,ejbHomeChargeForLowBalance方法检查余额并调用debit商业方法。下面是ejbHomeChargeForLowBalance方法的源代码:

public void ejbHomeChargeForLowBalance(
BigDecimal minimumBalance, BigDecimal charge)
throws InsufficientBalanceException {

try {
SavingsAccountHome home =
(SavingsAccountHome)context.getEJBHome();
Collection c = home.findInRange(new BigDecimal(""0.00""),
minimumBalance.subtract(new BigDecimal(""0.01"")));

Iterator i = c.iterator();

while (i.hasNext()) {
SavingsAccount account = (SavingsAccount)i.next();
if (account.getBalance().compareTo(charge) == 1) {
account.debit(charge);
}
}

} catch (Exception ex) {
throw new EJBException(""ejbHomeChargeForLowBalance: ""
+ ex.getMessage());
}
}

home接口中定义了相应的名为chargeForLowBalance的方法 (参见Home方法定义)。接口提供了客户端视图,在SavingsAccountClient程序中将这样调用home方法:

SavingsAccountHome home;
. . .
home.chargeForLowBalance(new BigDecimal(""10.00""),
new BigDecimal(""1.00""));

在entity bean类中,一个home方法的实现必须遵守以下规则:
1、一个home方法名必须以前缀ejbHome开始。
2、访问控制修饰必须是public。
3、方法修饰不能是static。
throws子句可以包含你的应用程序中指定的例外;它不可以抛出java.rmi.RemoteException。

数据库访问
表5-1概括了SavingsAccountBean类中所实现的数据库访问。在张表中不包含SavingsAccountBean类中的商业方法,这是因为它们不访问数据库。这些商业方法对实例变量进行更新,在EJB容器调用ejbStore时,这些实例变量将被写入到数据库中。其它开发者可能会选择在SavingsAccountBean类的商业方法中访问数据库。你使用哪种方法需要根据你的应用程序的具体需要而定。
表5-1 SavingsAccountBean中的SQL语句
方法:SQL语句
ejbCreate:INSERT
ejbFindByPrimaryKey:SELECT
ejbFindByLastName:SELECT
ejbFindInRange:SELECT
ejbLoad:SELECT
ejbRemove:DELETE
ejbStore:UPDATE
在访问数据库前,你必须首先连接数据库。详细信息请参看第十六章。

Home接口
home接口定义了允许客户端创建和寻找一个entity bean的方法。SavingsAccountHome接口的代码如下所示:

import java.util.Collection;
import java.math.BigDecimal;
import java.rmi.RemoteException;
import javax.ejb.*;

public interface SavingsAccountHome extends EJBHome {

public SavingsAccount create(String id, String firstName,
String lastName, BigDecimal balance)
throws RemoteException, CreateException;

public SavingsAccount findByPrimaryKey(String id)
throws FinderException, RemoteException;

public Collection findByLastName(String lastName)
throws FinderException, RemoteException;

public Collection findInRange(BigDecimal low,
BigDecimal high)
throws FinderException, RemoteException;

public void chargeForLowBalance(BigDecimal minimumBalance,
BigDecimal charge)
throws InsufficientBalanceException, RemoteException;
}

create方法定义
在home接口中的每一个create方法都必须符合以下条件:
1、参数的数量和类型与enterprise bean中相应的ejbCreate方法匹配。
2、返回enterprise bean的remote接口类型。
3、throws子句包含在相应的ejbCreate方法和ejbPostCreate方法的throws子句中指定的例外。
4、throws子句包含javax.ejb.CreateException。
5、如果这个方法是在一个远程home接口中定义的,而不是在本地home接口中定义的,throws子句必须包含java.rmi.RemoteException。

Finder方法定义
home接口中的每一个finder方法与entity bean类中的一个finder方法相对应。home接口中的finder方法名以find开头,在entity bean中相应的方法名以ejbFind开头。例如,在SavingsAccountHome类中定义了findByLastName方法,SavingsAccountBean类中相应的方法为ejbFindByLastName。home接口中的finder方法必须遵守以下规则:
1、参数的数量和类型与entity bean类中相应的方法匹配。
2、返回值类型是entity bean的remote接口,或者是这种接口的结果集。
3、throws子句包含entity bean类中相应方法的throws子句中的例外。
4、throws子句包含javax.ejb.FinderException。
5、如果这个方法是定义在一个远程home接口,而不是本地home接口中,throws子句包含java.rmi.RemoteException。

Home方法定义
home接口中定义的每一个home方法都对应entity bean类中的一个方法。在home接口中,方法名可以任意指定,只是不能以create或find开头。在bean类中,相应的方法名以ejbHome开头。例如,在SavingsAccountBean类中方法名为ejbHomeChargeForLowBalance,而在SavingsAccountHome接口中,相应的方法为chargeForLowBalance。
home方法必须遵守的规则与前面的finder方法相同(当然一个home方法不抛出一个FinderException)。

Remote接口
remote接口继承自javax.ejb.EJBObject,其中定义了远程客户端可以调用的商业方法。下面是SavingsAccount的remote接口的源代码:

import javax.ejb.EJBObject;
import java.rmi.RemoteException;
import java.math.BigDecimal;

public interface SavingsAccount extends EJBObject {

public void debit(BigDecimal amount)
throws InsufficientBalanceException, RemoteException;

public void credit(BigDecimal amount)
throws RemoteException;

public String getFirstName()
throws RemoteException;

public String getLastName()
throws RemoteException;

public BigDecimal getBalance()
throws RemoteException;
}

对于session bean和entity bean,在一个remote接口中定义的方法所必须满足的条件是相同的:
1、remote接口的每一个方法必须与enterprise bean类中的一个方法相匹配。
2、remote接口中的方法必须与enterprise bean类中相应的方法相同。
3、参数和返回值必须是有效的RMI类型。
4、throws子句必须包含java.rmi.RemoteException。
除了以下几个方面,一个本地接口也必须满足以上条件:
1、参数和返回值不需要是有效的RMI类型。
2、throws子句不包含java.rmi.RemoteException。

运行SavingsAccountEJB示例

设置数据库
下面将说明如何在SavingsAccountEJB示例中使用一个Cloudscape数据库。Cloudscape软件包含在J2EE SDK下载中。
1、在命令行中,输入cloudscape -start以启动Cloudscape数据库(当你准备关闭这个数据库时,输入cloudscape -stop。)
2、创建savingsaccount数据表。
a、到j2eetutorial/examples目录下。
b、输入ant create-savingsaccount-table。
你也可以在运行这个示例时不使用Cloudscape。(参见J2EE SDK支持的数据库列表。) 如果你是使用的其它数据库,你可以运行j2eetutorial/examples/sql/savingsaccount.sql脚本以创建savingsaccount数据表。

部署应用程序
1、在deploytool,打开j2eetutorial/examples/ears/SavingsAccountApp.ear文件(FileOpen)。
2、部署SavingsAccountApp应用程序(ToolsDeploy)。在Introduction对话框,查看是否选择了Return Client JAR检验栏。详细信息,请参看部署J2EE应用程序。

运行客户端
1、在一个终端窗口,到j2eetutorial/examples/ears 目录下。
2、将APPCPATH环境变量设置为 SavingsAccountAppClient.jar。
3、在同一行中输入以下命令:
runclient -client SavingsAccountApp.ear -name
SavingsAccountClient -textauth
4、在login提示符后,输入guest作为用户名,guest123作为口令。
5、客户端会显示以下信息:
balance = 68.25
balance = 32.53
456: 44.77
730: 19.54
268: 100.07
836: 32.55
456: 44.77
4.00
7.00

使用Bean管理持续化的Entity Bean的deploytool提示
在第四章我们对一个session bean的创建和打包作了详细的介绍。要构建一个entity bean,你需要执行相同的步骤,但是有以下几点不同。
1、在New Enterprise Bean向导中,指定bean的类型和持续化状态。
a、在General对话框,选择Entity按钮。
b、在Entity Settings对话框,选择Bean-Managed Persistence按钮。
2、在Resource Refs标签下,指定bean所引用的资源。这些设置使得bean可以连接到数据库。有关用法,请参见资源引用的deploytool提示。
3、在你部署bean之前,检查JNDI命名是否正确。
a、选择应用程序。
b、选择JNDI命名标签

Bean管理持续化中映射表的关联关系
在一个关系型数据库中,表可以通过共同的列关联。表之间的关联关系将影响它们相应的entity bean。我们在这里所讨论的entity bean受具有以下几种关联类型的表的支持:
1、一对一
2、一对多
3、多对多

一对一关系
在一个一对一关系中,一个表中的第一行与另一个表中单独的一行关联。例如,在一个仓储管理应用程序中,一个storagebin表与一个widget表之间就存在这样的一对一关系。这个应用程序会描述这些一个仓库,在每一个箱柜中只包含一种货物,而一种货物只会存在于一个箱柜中。
图5-1说明了表storagebin和widget之间的关系。storagebin表中的一行的唯一的序列号storagebinid是这个表的主键,而widgetid是表widget的主键。因为widgetid也是storagebin表中的一列,所以两个表之间具有关联关系。通过对照widget表中的主键,storagebin表中的widgetid表明了在仓库中某一货柜中存放的货物。因为表storagebin中的widgetid指向了另一个表的主键,它被称之为foreign key(外键)。(在图中我们用PK表示主键,FK表示外键。)
图5-1 一对一表间关联

子表包含一个匹配父表的主键的外键。在表storagebin(子表)中外键的值取决于表widget(父表)中的主键。例如,如果表storagebin中有一行的widgetid为344,那么表widget也会有widgetid同样为344的一行。
在设计一个数据库应用程序时,你可以选择强制父表和子表之间的依赖关系。有两种方法来实现这种强制:在数据库中定义一个关联约束或者是通过应用程序代码来执行检查。在表storagebin中有一个名这fk_widgetid的关联约束:

CREATE TABLE storagebin
(storagebinid VARCHAR(3)
CONSTRAINT pk_storagebin PRIMARY KEY,
widgetid VARCHAR(3),
quantity INTEGER,
CONSTRAINT fk_widgetid
FOREIGN KEY (widgetid)
REFERENCES widget(widgetid));

在j2eetutorial/examples/src/ejb/storagebin目录下有下面这个示例的源代码。要编译这个代码,到j2eetutorial/examples目录下输入ant storagebin。在j2eetutorial/examples/ears目录下有一个StorageBinApp.ear文件的示例。
StorageBinBean类和WidgetBean类阐明了数据表storagebin和widget之间的一对一的关联关系。StorageBean类中包含了对应于storagebin数据表中每一列的变量,包括外键--widgetId:

private String storageBinId;
private String widgetId;
private int quantity;

StorageBean类的ejbFindByWidgetId方法可以根据一个给定的widgetId返回与之相匹配的storageBinId:

public String ejbFindByWidgetId(String widgetId)
throws FinderException {

String storageBinId;

try {
storageBinId = selectByWidgetId(widgetId);
} catch (Exception ex) {
throw new EJBException(""ejbFindByWidgetId: "" +
ex.getMessage());
}

if (storageBinId == null) {
throw new ObjectNotFoundException
(""Row for widgetId "" + widgetId + "" not found."");
}
else {
return storageBinId;
}
}

ejbFindByWidgetId方法通过在selectByWidgetId方法中查询数据以定位widgetId:

private String selectByWidgetId(String widgetId)
throws SQLException {

String storageBinId;

String selectStatement =
""select storagebinid "" +
""from storagebin where widgetid = ? "";
PreparedStatement prepStmt =
con.prepareStatement(selectStatement);
prepStmt.setString(1, widgetId);

ResultSet rs = prepStmt.executeQuery();

if (rs.next()) {
storageBinId = rs.getString(1);
}
else {
storageBinId = null;
}

prepStmt.close();
return storageBinId;
}

要找出一种货物存放在哪一个货柜中,StorageBinClient程序中调用了findByWidgetId方法:

String widgetId = ""777"";
StorageBin storageBin =
storageBinHome.findByWidgetId(widgetId);
String storageBinId = (String)storageBin.getPrimaryKey();
int quantity = storageBin.getQuantity();

运行StorageBinEJB示例
1、创建storagebin数据表。
a、到j2eetutorial/examples目录下。
b、输入ant create-storagebin-table。
2、部署StorageBinApp.ear文件(位于j2eetutorial/examples/ears目录下)。
3、运行客户端。
a、到j2eetutorial/examples/ears目录下。
b、将APPCPATH环境变量设置为 StorageBinAppClient.jar。
c、在一行中输入以下命令:
runclient -client StorageBinApp.ear -name StorageBinClient
-textauth
d、在提示登录时,输入guest作为用户名,guest123作为口令。

一对多关系
如果在一个交表中的主键匹配一个子表中的多个外键,这种关联关系就是一对多的关系。这种关联关系在数据库应用程序也是比较常见的。例如,一个有关体育联盟的应用程序可能会访问一个team表和一个player表。每一个队有多个队员,而每一个队员只能属于一个队。在子表(player)中的每一行都有一个外键指向队员所在队。这个外键与team表的主键相匹配。
下面我们将讨论如何在entity bean中实现一对多的关联关系。当你设计这样的entity bean时,你必须决定是两个表都用entity bean来表现,还是只表现其中一个。

用于子表的一个辅助类
不是每一个数据库的表都必须映射为一个entity bean。如果一个数据表不反映一个商业对象,或者它存储的信息在另一个实体中以有所反映,这个表可以通过一个辅助类来表现。例如,在一个在线商店的应用程序中,每个由顾客提交的订单可以包含多个项目。这个应用程序存储在数据表中的信息如图5-2所示。
图5-2 一对多关系:订单和项目

不仅一个项目只属于一个订单,而且离开了订单,项目也不能独立存在。因此,lineitems表可以用一个辅助类来表现,而不必使用entity bean。在这种情况下使用一个辅助类并不是唯一的选择,但是由于辅助类与一个entity bean占用的系统资源更少,这样做可以提高性能。
在j2eetutorial/examples/src/ejb/order目录下有以下这个示例的源代码。要编译这个代码,到j2eetutorial/examples目录下,输入ant order。在j2eetutorial/examples/ears目录下有一个OrderApp.ear文件的示例。
LineItem类和OrderBean类使用了辅助类(LineItem)实现了一个一对多的关系。LineItem类中的实例变量对应了lineitems表中的列。itemNo变量匹配了lineitems表的主键,orderId变量对应了表的外键。下面是LineItem类的源代码:

public class LineItem implements java.io.Serializable {

String productId;
int quantity;
double unitPrice;
int itemNo;
String orderId;


public LineItem(String productId, int quantity,
double unitPrice, int itemNo, String orderId) {

this.productId = productId;
this.quantity = quantity;
this.unitPrice = unitPrice;
this.itemNo = itemNo;
this.orderId = orderId;
}

public String getProductId() {
return productId;
}

public int getQuantity() {
return quantity;
}

public double getUnitPrice() {
return unitPrice;
}

public int getItemNo() {
return itemNo;
}

public String getOrderId() {
return orderId;
}
}

OrderBean类包含了一个名为lineItems的ArrayList变量。lineItems变量中的每个元素是一个LineItem对象。lineItems变量在ejbCreate方法中被传递到OrderBean类。对于lineItems变量中的每一个LineItem对象,ejbCreate方法都相应地在lineitems表中添加一行。同时对于每一个实例,也会在orders表中添加一行。ejbCreate方法的代码如下:

public String ejbCreate(String orderId, String customerId,
String status, double totalPrice, ArrayList lineItems)
throws CreateException {

try {
insertOrder(orderId, customerId, status, totalPrice);
for (int i = 0; i < lineItems.size(); i++) {
LineItem item = (LineItem)lineItems.get(i);
insertItem(item);
}
} catch (Exception ex) {
throw new EJBException(""ejbCreate: "" +
ex.getMessage());
}

this.orderId = orderId;
this.customerId = customerId;
this.status = status;
this.totalPrice = totalPrice;
this.lineItems = lineItems ;

return orderId;
}

OrderClient程序创建并装载了一个LineItem对象的ArrayList。当它调用create时将这个ArrayList传递给entity bean:

ArrayList lineItems = new ArrayList();
lineItems.add(new LineItem(""p23"", 13, 12.00, 1, ""123""));
lineItems.add(new LineItem(""p67"", 47, 89.00, 2, ""123""));
lineItems.add(new LineItem(""p11"", 28, 41.00, 3, ""123""));
. . .
Order duke = home.create(""123"", ""c44"", ""open"",
totalItems(lineItems), lineItems);

在OrderBean类中的其它方法也同时访问两个数据表。例如,对于ejbRemove方法,其结果不仅仅是删除orders表中的一行,而是同时删除lineitems表中所有相应的行。ejbLoad方法和ejbStore方法将处理包含lineItems ArrayList的OrderEJB实例状态与表orders及表lineitems的同步。

ejbFindByProductId方法用来供客户端定位所有具有指定项目的订单。这个方法查询lineitems表中所有productId为指定值的行。这个方法返回一个productId String对象的Collection。OrderClient程序对Collection进行遍历并打印出每个订单的主键:

Collection c = home.findByProductId(""p67"");
Iterator i=c.iterator();
while (i.hasNext()) {
Order order = (Order)i.next();
String id = (String)order.getPrimaryKey();
System.out.println(id);
}

运行OrderEJB示例
1、创建orders数据表:
a、到j2eetutorial/examples/src目录下。
b、输入ant create-order-table。
2、部署OrderApp.ear文件(位于j2eetutorial/examples/ears目录下)。
3、运行客户端。
a、到j2eetutorial/examples/ears 目录下。
b、设置APPCPATH环境变量为OrderAppClient.jar。
c、在同一行内输入以下命令:
runclient -client OrderApp.ear -name OrderClient
-textauth
d、在提示登录时,输入guest作为用户名,guest123作为口令。

子表的一个Entity Bean
当一个子表满足以下条件时,你应该考虑为它创建一个entity bean:
1、子表中的信息不依赖于父表。
2、如果在父表中不存在,子表中的商业对象也可以独立存在。
3、子表可能会被一个不访问父表的应用程序访问。
在下面的例子中就存在这种情况。假定在一个公司中每一个销售代表都有多个客户,而每一个客户只能有一个销售代表。而公司要开发一个数据库应用程序以了解它的销售力量。在这个数据库中,salesrep表(父表)的每一行匹配customer表(子表)的多行。图5-3描述了这种关联关系。
图5-3 一对多关系:销售代表和客户

SalesRepBean和CustomerBean这两个entity bean类实现了表sales和表customer之间的一对多的关系。
在j2eetutorial/examples/src/ejb/salesrep目录下有这个示例的源代码。要编译这个代码,到j2eetutorial/examples目录下,输入ant salesrep。在j2eetutorial/examples/ears目录下有SalesRepApp.ear文件的示例。
SalesRepBean类中包含了一个名为customerIds的变量,这是一个StringArrayList(字符串数组)。这些字符串标明了客户属于哪个销售代表。因为customerIds变量反映了这个关联关系,SalesRepBean类必须保证这个变量是实时更新的。
SalesRepBean类在setEntityContext方法中对customerIds变量进行了例示,而不是象通常那样在ejbCreate方法中完成这种例示。容器只会调用setEntityContext方法一次--当创建bean的实例时--这确保了customerIds只会被初始化一次。因为同一个bean的实例在它的生命周期中可以采取不同的序列号,在ejbCreate方法中例示customerIds会导致多次的不必要的例示。因此SalesRepBean类中对customerIds变量的例示是在setEntityContext方法中进行的:

public void setEntityContext(EntityContext context) {

this.context = context;
customerIds = new ArrayList();

try {
makeConnection();
Context initial = new InitialContext();
Object objref =
initial.lookup(""java:comp/env/ejb/Customer"");

customerHome =
(CustomerHome)PortableRemoteObject.narrow(objref,
CustomerHome.class);
} catch (Exception ex) {
throw new EJBException(""setEntityContext: "" +
ex.getMessage());
}
}

由ejbLoad方法所调用的loadCustomerIds是一个私有的方法,它用来刷新customerIds变量。在编写loadCustomerIds这样的方法时有两种做法:从customer数据表中获取序列号或者是从CustomerEJB这个entity bean中获取序列号。从数据库中获取序列号也许可以提高速度,但是SalesRepBean类的代码将受到CustomerEJB bean的底层数据库的影响。如果将来你想要改变CustomerEJB bean的数据表(或者是将这个bean移动到另一个J2EE服务器),你可能需要改变SalesRepBean的代码。但是如果SalesRepBean类是从CustomerEJB entity bean中获取序列号,就不必对代码进行改动。这两种做法就体现了一种平衡;性能和灵活性。我们的示例中选择了灵活性:通过调用CustomerEJB的findSalesRep方法和getPrimaryKey方法以获取customerIds变量。这儿是loadCustomerIds方法的源代码:

private void loadCustomerIds() {

customerIds.clear();

try {
Collection c = customerHome.findBySalesRep(salesRepId);
Iterator i=c.iterator();

while (i.hasNext()) {
Customer customer = (Customer)i.next();
String id = (String)customer.getPrimaryKey();
customerIds.add(id);
}

} catch (Exception ex) {
throw new EJBException(""Exception in loadCustomerIds: "" +
ex.getMessage());
}
}

如果一个客户改变了销售代表,客户端程序将通过调用CustomerBean类中的setSalesRepId方法更新数据库。那么在SalesRepBean类中的某一个商业方法一旦被调用,ejbLoad方法会调用loadCustomerIds,后者将会更新customerIds变量。(为确保在调用每一个商业方法之前都会调用ejbLoad,必须设置商业方法的事务属性。)例如,SalesRepClient程序为一个名为Mary Jackson的客户改变了salesRepId:

Customer mary = customerHome.findByPrimaryKey(""987"");
mary.setSalesRepId(""543"");

salesRepId值543确定了一个名为Janice Martin的销售代表。要列出Janice的所有客户,SalesRepClient程序将调用getCustomerIds方法遍历数组通过调用findByPrimaryKey方法以定位每一个CustomerEJB entity bean:

SalesRep janice = salesHome.findByPrimaryKey(""543"");
ArrayList a = janice.getCustomerIds();
i = a.iterator();

while (i.hasNext()) {
String customerId = (String)i.next();
Customer customer =
customerHome.findByPrimaryKey(customerId);
String name = customer.getName();
System.out.println(customerId + "": "" + name);
}

运行SalesRepEJB示例
1、创建数据表。
a、到j2eetutorial/examples/src目录下。
b、输入ant create-salesrep-table。
2、部署SalesRepApp.ear文件(位于j2eetutorial/examples/ears目录下)。
3、运行客户端。
a、到j2eetutorial/examples/ears目录下。
b、设置APPCPATH环境变量的值为SalesRepAppClient.jar。
c、在一行中输入以下命令:
runclient -client SalesRepApp.ear -name SalesRepClient
-textauth
d、在提示登录时,输入guest作为用户名,guest123作为口令。

多对多关系
在一个多对多关系中,每一个each entity可能与多个其它实体关联。例如,一个大学课程可以有多名学生,而每一个学生也可能同时选择几门课程。在一个数据库中,这种关联关系是能过一个包含外键的对照表来描述的。在图5-4中,对照表是enrollment表。这些表可以通过StudentBean类、CourseBean类和EnrollerBean类访问。
图5-4 多对多关系:学生和课程

T在j2eetutorial/examples/src/ejb/enroller目录下有这个示例的源代码。要编译这个代码,到j2eetutorial/examples目录下,输入ant enroller。在j2eetutorial/examples/ears目录下有EnrollerApp.ear文件的示例。
StudentBean类和CourseBean类是相补的。每一个类都包含外键的数组。例如,在StudentBean类中,包含了一个名为courseIds的数组,它标志了学生所参与的课程。同样的,CourseBean类也包括了一个名为studentIds的数组。
StudentBean类的ejbLoad方法通过调用 loadCourseIds这个私有方法以增加courseIds ArrayList的元素。loadCourseIds方法从EnrollerEJB这个session bean中获取课程标识符。loadCourseIds方法的源代码如下:

private void loadCourseIds() {

courseIds.clear();

try {
Enroller enroller = enrollerHome.create();
ArrayList a = enroller.getCourseIds(studentId);
courseIds.addAll(a);

} catch (Exception ex) {
throw new EJBException(""Exception in loadCourseIds: "" +
ex.getMessage());
}
}

loadCourseIds方法所调用的EnrollerBean类的getCourses方法对enrollment表进行查询:

select courseid from enrollment
where studentid = ?

只有EnrollerBean类可以访问enrollment表。因此,EnrollerBean类管理了enrollment表中所反映的学生和课程之间的关联关系。例如,如果一名学生选择了一门课程,客户端会调用enroll商业方法,它将向数据库中添加一行:

insert into enrollment
values (studentid, courseid)

如果一名学生放弃了一门课程,unEnroll将删除一行:

delete from enrollment
where studentid = ? and courseid = ?

如果一名学生离开了学校,deleteStudent方法将删除表中所有与该学生相关的行:

delete from enrollment
where student = ?

EnrollerBean类不删除student表中相关的行。这个工作是由StudentBean类中的ejbRemove方法来完成的。为了确保两个删除在同一个操作中完成,它们需要属于同一事务。详细信息请参见第十四章。

运行EnrollerEJB示例
1、创建数据表。
a、到j2eetutorial/examples目录下。
b、输入ant create-enroller-table。
2、部署EnrollerApp.ear文件(位于j2eetutorial/examples/ears目录下)。
3、运行客户端。
a、到j2eetutorial/examples/ears目录下。
b、设置APPCPATH环境变量为EnrollerAppClient.jar。
c、在同一行中输入以下命令:
runclient -client EnrollerApp.ear -name EnrollerClient
-textauth
d、在提示登录时,输入guest作为用户名,guest123作为口令。

Bean管理持续化的主键
你必须在entity bean的部署描述中指定主键类。在绝大多数情况下,你的主键类可能是一个String、一个Integer或其它属于J2SE和J2EE标准库的类。但是对于一些entity bean,你可能需要定义你自己的主键类。例如,如果这个bean具有一个复合主键(也就是说,由几个字段组合而成的主键),那么你需要创建一个主键类。

主键类
下面的主键类是一个复合键--productId和vendorId字段共同标识了一个entity bean。

public class ItemKey implements java.io.Serializable {

public String productId;
public String vendorId;

public ItemKey() { };

public ItemKey(String productId, String vendorId) {

this.productId = productId;
this.vendorId = vendorId;
}

public String getProductId() {

return productId;
}

public String getVendorId() {

return vendorId;
}

public boolean equals(Object other) {

if (other instanceof ItemKey) {
return (productId.equals(((ItemKey)other).productId)
&& vendorId.equals(((ItemKey)other).vendorId));
}
return false;
}

public int hashCode() {

return productId.concat(vendorId).hashCode();
}
}

对于bean管理持续化,一个主键类必须满足以下条件:
1、类的访问控制修饰必须是public。
2、所有的字段都必须申明为public。
3、这个类必须有一个public类型的默认的构造函数。
4、类必须实现hashCode()和equals(Object other)方法。
5、类必须可序列化。

Entity Bean类中的主键
在bean管理持续化中,ejbCreate方法将输入参数赋值给实例变量并返回主键类:

public ItemKey ejbCreate(String productId, String vendorId,
String description) throws CreateException {

if (productId == null'' ''vendorId == null) {
throw new CreateException(
""The productId and vendorId are required."");
}

this.productId = productId;
this.vendorId = vendorId;
this.description = description;

return new ItemKey(productId, vendorId);
}

ejbFindByPrimaryKey方法检查给定主键的行在数据库中是否存在:

public ItemKey ejbFindByPrimaryKey(ItemKey primaryKey)
throws FinderException {

try {
if (selectByPrimaryKey(primaryKey))
return primaryKey;
...
}

private boolean selectByPrimaryKey(ItemKey primaryKey)
throws SQLException {

String selectStatement =
""select productid "" +
""from item where productid = ? and vendorid = ?"";
PreparedStatement prepStmt =
con.prepareStatement(selectStatement);
prepStmt.setString(1, primaryKey.getProductId());
prepStmt.setString(2, primaryKey.getVendorId());
ResultSet rs = prepStmt.executeQuery();
boolean result = rs.next();
prepStmt.close();
return result;
}

获取主键
一个客户端可以通过调用EJBObject类中的getPrimaryKey方法以获取一个entity bean的主键:

SavingsAccount account;
...
String id = (String)account.getPrimaryKey();

entity bean通过调用EntityContext类的getPrimaryKey方法以获取它自身的主键:

EntityContext context;
...
String id = (String) context.getPrimaryKey();

处理例外
enterprise bean抛出的例外分为两种:系统级的和应用程序级的。
一个系统级的例外反映了支持一个应用程序的服务器的问题。这些问题可能包括:不能连接数据库、因为数据库溢出导致插入失败或者是lookup方法不能找到想得到的对象。如果你的enterprise bean遇到了一个系统级的问题,它会抛出了一个javax.ejb.EJBException。容器会将EJBException包装到一个RemoteException中,后者将被返回到客户端。因为EJBException是RuntimeException的子类,所以你不需要在方面声明的throws子句中指定它。如果抛出了一个系统级的例外,EJB容器可能会销毁bean的实例。因此,一个系统级的例外不能由客户端的应用程序处理;它需要系统管理员的干涉。
一个应用程序级的例外反映了一个enterprise bean中的商业逻辑发生的逻辑。应用程序级的例外有两种:自定义的和预定义的。一个自定义的例外是你自己编写的,例如SavingsAccountEJB示例中debit商业方法抛出的InsufficentBalanceException。javax.ejb软件包包含了几个预定义的用以处理常见问题的例外。例如,一个ejbCreate方法在接收到一个无效的输入参数时会抛出一个CreateException。当一个enterprise bean抛出一个应用程序级的例外时,容器不会将其包装到另一个例外中。客户端可以处理它所获知的任何应用程序级的例外。
如果在一个事务中发生一个系统级的例外,EJB容器会回滚整个事务。然而,如果发生的应用程序级的例外,容器不会回滚事务。
表5-2概括了javax.ejb软件包的例外。只有NoSuchEntityException和EJBException是系统级的例外,其它所有的这些例外都是应用程序级的例外。
表5-2 例外
方法名:抛出的例外:导致例外的原因
ejbCreate:CreateException:输入参数无效。
ejbFindByPrimaryKey(以及其它返回单个对象的finder方法):ObjectNotFoundException(FinderException的子例):在数据库中未找到对应于请求entity bean的行。
ejbRemove:RemoveException:entity bean对应的行不能从数据库中删除。
ejbLoad:NoSuchEntityException:不能找到被载入的数据行。
ejbStore:NoSuchEntityException:不能找到被更新的数据行。
(所有方法):EJBException:遇到了一个系统级问题。