译者前言
有关本章中相应源代码的下载,请参看一个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方法的[url=http://www.pccode.net].net" class="wordstyle"源码比较短:
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 BigDe