作者:自由的猪 制作整理:左岸网络http://www.leftworld.net

第5章 BMP的例子

Dale Green 著

IceShape Zeng 译

数据是大多数商业应用程序的核心(这个好像是废话,没有数据程序也只能玩玩了)。在J2EE应用程序中,实体Bean表示存储在数据库中的商业实体。如果用BMP实现实体Bean,你必须自己编写数据库访问代码。虽然写这些代码是附加的责任,但是因此你可以更灵活的控制实体Bean访问数据库的行为(当然这得你自己愿意才行)。

本章讨论BMP实现实体Bean的编码技术。关于实体Bean的概念请参考第3章企业Bean中实体Bean一节。

本章内容:

1.SavingsAccountEJB

实体Bean类

Home接口

Remote接口

运行该例子

2.用deploytool部署BMP实现的实体Bean

3.为BMP映射表间关系

一对一关系

一对多关系

多对多关系

4.BMP的主键

主键类

实体Bean中的主键

获取主键

5.处理异常

 

一.SavingsAccountEJB

本例中的实体Bean表示一个样本银行账号。SavingsAccountEJB的状态信息保存在一个关系数据库的savingaccount表中。创建该表的SQL语句如下:

CREATE TABLE savingsaccount
   (id VARCHAR(3) 
   CONSTRAINT pk_savingsaccount PRIMARY KEY,
   firstname VARCHAR(24),
   lastname  VARCHAR(24),
   balance   NUMERIC(10,2));

SavingsAccountEJB由三个文件组成:

☆ 实体Bean类(SavingsAccountBean)

☆ Home接口(SavingsAccountHome)

☆ Remote接口(SavingsAccount)

本例应用程序还包括下面的两个类:

☆ 一个异常类InsufficientBalanceException

☆ 一个客户端类SavingsAccountClient

实体Bean类(SavingsAccountBean)

SavingsAccount是本例中的实体Bean类。从它的代码可以看出BMP实现实体Bean的基本要求。首先实现一下接口和方法:

☆ EntityBean接口

☆ 大于等于零对的ejbCreate和ejbPostCreate方法(实体Bean可以没有ejbCreate方法,只有查找方法,但是不能两者都没有)

☆ 查找(Finder)方法

☆ 商业方法

☆ Home方法(这里的Home方法不好理解,如果是指生命周期方法,应该包含ejbcreate等方法)

另外它还有如下的一些特征:

☆ 该类访问属性为public

☆ 该类不可以被定义为abstract或者final

☆ 该类包含一个空构造函数

☆ 该类不可以实现finalize方法

EntityBean接口

该接口也继承至EnterpriseBean接口(EnterpriseBean接口是SessionBean和EntityBean共同的父接口,它继承至Serializable接口,没有任何方法)。EntityBean定义一些方法,如ejbActive、ejbLoad和ejbStore等等,你必须在实体Bean类里实现它们。EntityBean接口的定义如下:

package javax.ejb;

import java.rmi.RemoteException;

// Referenced classes of package javax.ejb:

// EnterpriseBean, EJBException, RemoveException, EntityContext

public interface EntityBean

    extends EnterpriseBean

{

    public abstract void setEntityContext(EntityContext entitycontext)

        throws EJBException, RemoteException;

    public abstract void unsetEntityContext()

        throws EJBException, RemoteException;

    public abstract void ejbRemove()

        throws RemoveException, EJBException, RemoteException;

    public abstract void ejbActivate()

        throws EJBException, RemoteException;

    public abstract void ejbPassivate()

        throws EJBException, RemoteException;

    public abstract void ejbLoad()

        throws EJBException, RemoteException;

    public abstract void ejbStore()

        throws EJBException, RemoteException;

}

ejbCreate方法

当客户端调用create方法后,EJB容器调用对应的ejbCreate方法。实体Bean中典型的ejbCreate方法完成如下工作:

☆ 将实体状态(表述实体Bean的属性字段)插入数据库

☆ 初始化实例变量,就是对实体Bean的属性字段赋值

☆ 返回主键

本例中SavingsAccountBean的ejbCreate方法调用私有方法insertRow来将实体状态插入数据库,insertRow方法向数据库发出一条INSERT的SQL命令。下面是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方法,但是一个企业Bean可以有多个ejbCreate方法。例如前一章CartEJB的例子。

编写实体Bean的ejbCreate方法许要遵循如下规则:

☆ 访问权修饰符必须是public

☆ 返回值类型必须是主键类

☆ 参数类型必须符合RMI调用规则

☆ 该方法不可以声明为final或者static

throws子句要包含javax.ejb.CreateException异常,通常遇到非法参数ejbCreate方法会抛出该异常。如果ejbCreate方法因为存在使用相同主键的其他实体而无法创建实体,将抛出javax.ejb.DuplicateKeyException异常,它是CreateException的子类。如果客户端捕获CreateException或者DuplicateKeyException异常,就表示实体创建失败。

实体Bean的状态数据可能被J2EE服务器不知道的其他应用程序直接插入到数据库里。例如,你可以直接用SQL语句在数据库工具中插入一行到savingsaccount表里。虽然该行对应的实体Bean没有被ejbCreate方法创建,但客户端仍然可以找到该行对应的实体Bean(不用说,这里当然是用ejbFinder方法了,下面会介绍)。

EjbPostCreate方法

对每一个ejbCreate方法,你都必须在实体Bean中写一个对应的ejbPostCreate方法。因为EJB容器在调用ejbCreate方法后接着就调用ejbPostCreate方法。跟ejbCreate方法不同的是,ejbPostCreate方法可以调用EntityContext接口的getPrimaryKey和getEJBObject方法(在前一章的传递企业Bean对象的引用一节讨论过)。EjbPostCreate方法大部分情况下什么事也不干。

EjbPostCreate方法声明必须符合一下要求:

☆ 参数数量类型声明顺序必须跟对应的ejbCreate方法相同

☆ 必须声明为public

☆ 不能声明为final或者static

☆ 返回值必须是void

throws子句要包含javax.ejb.CreateException异常。

ejbRemove方法

客户端调用remove方法来删除实体Bean。该调用会引发EJB容器调用ejbRemove方法,ejbRemove方法从数据库中删除实体状态对应的数据。SavingsAccountBean的ejbRemove方法调用deleteRow私有方法向数据库发送一条DELETE的SQL命令。代码如下:

public void ejbRemove() {

    try {

        deleteRow(id);

    catch (Exception ex) {

        throw new EJBException("ejbRemove: " + ex.getMessage());

    }

}

如果ejbRemove方法遇到系统问题就抛出javax.ejb.EJBException异常。如果遇到一个应用程序异常就怕抛出javax.ejb.RemoveException异常。要区分系统异常和应用程序异常请参考异常处理一节。

实体Bean状态数据也可能被直接从数据库中删除。当用SQL语句删除数据库中实体Bean状态数据对应的行时,实体Bean也会被删除。

ejbLoad和ejbStore方法

如果EJB容器要同步实体Bean的属性子段和数据库中对应的数据就调用这两个方法。顾名思义,ejbLoad方法从数据库中取出数据并刷新属性子段的值,ejbStore方法把属性子段值写入数据库。客户端不能调用这两个方法。

如果商业方法在一个事务环境中执行,EJB容器会在该方法执行前调用ejbLoad,并在该方法执行完后立即调用ejbStore方法。这样你不必在商业方法中调用这两个方法来刷新和存储实体Bean数据。SavingsAccountBean依赖容器同步实体和数据库的数据,所以商业方法应该在事务环境中执行。

如果ejbLoad和ejbStore方法不能在数据库中找到实体的数据就会抛出javax.ejb.NosuchEntityException异常,它是EJBException的子类。因为它的父类是RutimeException的子类,所以你不必将该异常加到throws子句中。该异常在返回客户端前被容器封装进RemoteException的一个实例。

在SavingsAccountBean类中,ejbLoad调用LoadRow私有方法,后者向数据库发送一条SELECT的SQL命令并将读出的数据赋给SavingsAccountBean的属性字段。EjbStore调用storeRow私有方法,后者用UPDATE的SQL命令将SavingsAccountBean的属性字段值存入数据库。下面是这两个方法的代码:

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)

查找方法允许客户端查找实体Bean。SavingsAccountClient可以通过三个查找方法查找实体Bean:

SavingsAccount jones = home.findByPrimaryKey("836");

...

Collection c = home.findByLastName("Smith");

...

Collection c = home.findInRange(20.00, 99.00);

对每一个客户端可用的查找方法,实体Bean类必须事先一个对应的ejbFind为前缀的方法。SavingsAccountBean中对应上面findByLastName方法的ejbFindByLastNamef方法代码如下:

public Collection ejbFindByLastName(String lastName)

   throws FinderException {

   Collection result;

   try {

      result = selectByLastName(lastName);

   } catch (Exception ex) {

      throw new EJBException("ejbFindByLastName " + ex.getMessage());

   }

   return result;

}

查找方法的实现细节用应用程序决定,例如上例的ejbFindByLastName和ejbFindInRange方法的命名都是很随意的。但是ejbFindByPrimaryKey方法命名不能随意,就像它的名字所暗示的,该方法有一个用来查找实体Bean的主键类型的参数。SavingsAccountBean类中,主键是id子段。下面是SavingsAccountBean的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方法,而是由EJB容器代劳的。客户端只能调用在Home接口中声明的findByPrimaryKey方法。

下面总结一下BMP实现实体Bean的查找方法的规则:

☆ 必须实现ejbFindByPrimaryKey方法

☆ 方法名必须用ejbFind做前缀

☆ 方法不能用final或者static方法

☆ 如果BMP实现Remote接口组中的方法,则方法的参数和返回值类型必须是符合RMI API调用的合法类型

☆ 返回类型必须是主键类或者主键类的集合

Throws字句要包括javax.ejb.FinderException。如果查找方法返回一个主键类对象但是被查找的实体不存在,该方法将抛出javax.ejb.ObjectNotFoundException,该异常是javax.ejb.FinderException的子类。如果查找方法返回主键类对象集合,而且也没有符合要求的实体,则该方法返回空集合。

商业方法

商业方法处理你想封装在实体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();

会话Bean和实体Bean的商业方法的签名规则是相同的:

☆ 方法名不能和EJB体系定义的方法名冲突,比如商业方法不能命名为:ejbCreate或者ejbActivate

☆ 访问修饰必须是public

☆ Remote接口租中定义的方法的参数和返回值必须是RMI API规范的合法类型

Throws子句没有特殊要求。例如debit方法抛出InsufficientBalaceException自定义异常。为了捕获系统异常,商业方法可以抛出javax.ejb.EJBException。

Home方法

Home方法包含那些应用于一个特定企业Bean类的所有实例的商业逻辑。正好和只应用于由主键标志的企业Bean单个实例的商业方法相反。Home方法调用期间,企业Bean实例不仅没有唯一的标志主键,也不具备表示一个商业实体的状态。因此,Home方法不可以访问企业Bean的持久性字段(企业Bean中的实例变量)。(对于CMP,Home方法也不能访问关系字段。)

Home方法查找到企业Bean的实例的集合然后通过集合的迭代器调用商业方法是实现Home方法的典型做法。SavingsAccountBean类的ejbHomeChargeForLowBalance就是这样做的。该方法对余额少于某个特定数值的账户收取服务费,首先调用findInRange方法得到符合条件所有的账户,它返回一个SavingAccount实现类实例的集合,然后通过集合的迭代器访问这些远程接口对象,在检查了余额之后调用debit商业方法收取费用。下面是代码:

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方法定义将具体介绍)。客户端可以通过该接口访问Home方法:

SavingsAccountHome home;

...

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

综上所述,实体Bean类中Home方法的实现要遵循以下规则:

☆ 方法名必须以ejbHome开头

☆ 访问修饰符必须是public

☆ 方法不可以是static方法

根据应用程序逻辑确定throws子句,throws子句中不能包含java.rmi.RemoteException异常。

数据库访问

表5-1列出了SavingsAccountBean的数据库访问操作。商业方法并不在表中,因为它们不访问数据库,它们只是更新企业Bean的持久性子段,这些字段的值会在EJB容器调用ejbStore方法时被写入数据库。但是这并不是强制的规则,你也可以在商业方法中直接访问数据库来存取数据,这要根据你的应用程序的具体需要来决定。

访问数据库前你必须先得到数据库的连接,关于数据库连接的更多信息将在16章讨论。

表 5-1 SavingsAccountBean中的SQL语句 

方法

SQL 语句

ejbCreate

INSERT

ejbFindByPrimaryKey

SELECT

ejbFindByLastName

SELECT

ejbFindInRange

SELECT

ejbLoad

SELECT

ejbRemove

DELETE

ejbStore

UPDATE

Home接口

Home接口定义了让客户端创建和查找实体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方法

create方法的定义规则:

☆ 必须和企业Bean类中对应的ejbCreate方法有相同的参数数量、类型和排列顺序。就是要为每个Home中的定义的可用create方法在企业Bean中实现对应的ejbCreate方法。

☆ 返回企业Bean的远程接口类型

☆ throws子句包括对应的ejbCreate方法和ejbPostCreate方法的throws子句中出现的所有异常,另外还要包括javax.ejb.CreateException

☆ 如果方法是在Home接口而不是在LocalHome接口中定义的则throws子句必须包括java.rmi.RemoteException异常

定义查找方法

跟create的规则相似,Home接口中的每个查找方法都对应一个企业Bean类中的一个查找方法。Home接口中的查找方法必须以find开头,企业Bean类中对应的方法以ejbFind开头。本例中SavingsAccountHome接口定义的findByLastName对应的SavingsAccountBean类的方法为ejbFindByLastName方法。总结一下Home接口中的方法必须符合一下条件:

☆ 参数个数类型和顺序必须和对应的ejbFind方法相同

☆ 返回实体Bean远程接口类型,或是远程接口类型的集合

☆ throws字据除了包括对应的ejbFind方法的throws子句中出现的异常外,还要包括javax.ejb.FinderException

☆ 不是在Local Home接口中定义的方法throws子句还要包括java.rmi.RemoteExceotion异常

定义Home方法

Home接口中的每个Home方法都在实体Bean类中有一个对应的方法,这些方法的命名没有特殊的前缀,相反对应的实体Bean类中的方法要以ejbHome开头。如本例中SavingsAccountBean类中前面提到的ejbHomeChargeForLowBalance方法,在Home接口中的对应方法名为chargeForLowBalance。

除了Home方法不抛出FinderException,它的签名规则和查找方法是一样的。

Remote接口

Remote接口继承javax.ejb.EJBObject接口,定义远程客户端访问的商业方法。本例中SavingsAccount远程接口定义如下:

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;

}

会话Bean和实体Bean的远程方法的定义规则是相同的,这里再重复一下:

☆ 每个方法在企业Bean类中都必须有对应的方法

☆ 方法签名必须和企业Bean类中的对应方法相同

☆ 参数和返回值必须是符合RMI规范的类型

☆ throws子句企业Bean类对应方法的throws子句的基础上添加java.rmi.RemoteException

而Local接口有所不同:

☆ 参数和返回值不需要是RMI合法类型

☆ throws子句不需要包括java.rmi.RemoteException

运行本例子

配置数据库

本例使用Cloundscape数据库,该数据库软件被包括在J2EE SDK包里。

1. 启动数据库。在命令方式下执行如下命令

cloudscape –start

关闭命令为:cloudscape -stop

2. 创建savingsaccount表

a) 进入j2eetutorial/examples 目录

b) 执行ant create-savingsaccount-table 命令

你也可以用其他数据库来运行本例(要是J2EE服务器支持的数据库)。在其他数据库中创建该表要执行j2eetutorial/examples/sql/savingsaccount.sql 脚本文件。

部署应用程序

1.用deploy工具打开j2eetutorial/examples/ears/SavingsAccountApp.ear 文件

2.执行Tools\Deploy菜单命令部署。确定Introduction对话中你选中了Return Client JAR复选框

运行客户端

1.在命令方式下进入j2eetutorial/examples/ears 目录

2.设置APPPATH环境变量为SavingsAccountAppClient.jar 所在目录

3.执行以下命令(只有一条命令,有点长):

runclient -client SavingsAccountApp.ear -name SavingsAccountClient –textauth

4.在登录提示符下输入用户名guest,密码guest123

5.这一步什么也不做,看看结果:

balance = 68.25

balance = 32.55

456: 44.77

730: 19.54

268: 100.07

836: 32.55

456: 44.77

4.00

7.00

 二.用deploytool部署BMP实现的实体Bean

第4章介绍了创建一个会话Bean包的步骤,创建实体Bean包的步骤相似,但是有以下不同:

1. 在新建企业Bean向导(New Enterprise Bean)中,指定企业Bean类型和持久性管理类型

a) 在General对话框中,选定Entity单选项

b) 在Entity Settings对话框中,选定Bean-Managed Persistence

2. 在Resource Refs页,指定企业Bean引用的资源工厂。这些设置使企业Bean可以访问数据库。具体的设置信息参考用deploytool工具配置资源引用一节

3. 部署前,检查你的JNDI名是否都是对的

a) 从树中选择要部署的应用程序

b) 选择JNDI Names页

三.为BMP映射表间关系

在关系数据库中,数据表可以通过共同的列建立关系。数据库的关系影响了他们对应的实体Bean的设计。本节分以下几类讨论实体Bean如何映射数据库中的表间关系

☆ 一对一

☆ 一对多

☆ 多对多

一对一关系

一对一关系中一个表1的一行数据只对应于表2的一行数据,表2的一行数据也只对应表1的一行数据。例如:一个仓库应用程序中,储藏箱(storagebin)表和小物件(widget)表就可能是一对一关系。这个应用程序要为物理仓库中每个储藏箱只装一个小物件并且每个小物件也只能装在一个储藏箱中建立逻辑模型。

图5-1说明了两个表的关系。因为storagebinid字段可以唯一确定storagebin表中的一行,它是这个表的主键。widgetid是widget表的主键。storagebin表也有一个widgetid字段来关联两张表的数据。通过在storagebin表中引用widget表的主键,可以确定一个物件存储在仓库中的哪个储存箱中。因为storagebin中widget字段引用其他表的主键,所以它是一个外键。(本章的图例中用PK表示主键,FK表示外键。)

图 5-1 一对一关系

一般子表包含一个匹配父表数据的外键。子表storagebin中的外键widgetid的值依赖于父表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目录下执行storagebin命令编译这些文件。StorageBinApp.ear样本文件放在j2eetutorial/examples/ears目录下。

StorageBinBean和WidgetBean类文件实现了storagebin表和widget表之间的一对一关系。StorageBinBean类为storagebin表中包括外键widgetid的所有列生命了对应的实例变量:

private String storageBinId;

private String widgetId;

private int quantity;

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方法来得到数据库的查询结果(这种做法的好处是将商业方法和数据库操作方法分开,以使商业逻辑尽量的独立于数据库操作):

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;

}

客户端通过调用findByWidgetId方法(该方法在Home接口中定义,它对应与Bean类中的ejbFindByWindgetId方法)来找到物件存放的储藏箱:

String widgetId = "777";

StorageBin storageBin = storageBinHome.findByWidgetId(widgetId);

String storageBinId = (String)storageBin.getPrimaryKey();

int quantity = storageBin.getQuantity();

运行StorageBinApp应用程序

1. 创建storagebin数据表

a) 进入j2eetutorial/examples目录

b) 执行Type ant create-storagebin-table命令

2. 部署StorageBinApp.ear文件(在j2eetutorial/examples/ears directory目录下)

3. 运行客户端

a) 进入j2eetutorial/examples/ears目录

b) 将APPCPATH环境变量设置为StoreageBinAppClient.jar所在目录

c) 执行如下命令(只有一条命令)

runclient -client StorageBinApp.ear –name StorageBinClient -textauth

d) 在登录提示符下输入用户名guest和密码guest123

一对多关系

如果父表的一个主键值在子表中匹配多条纪录,就是一对多关系。一对多关系在数据库应用程序中经常出现。例如运动社团的应用中会有一个team(组)表和一个player(运动员)表。每个组中不会只有一个运动员,但每个运动员只属于一个组(这里只是假设情况,老外怎么可以这样开玩笑呢?只有当他根据不同应用灵活处理了)。在子表player中有一个外键标志它所属的组,这个外键匹配父表team中的主键。

对于一对多关系,在设计实体Bean的时候,最好考虑一下到底是将关系的双方都实现为实体Bean还是只将父表映射到实体Bean。(在《J2EE核心模式》中有详细论述)下面的例子用实体Bean实现一对多的关系。

将子表实现为辅助类

并不是数据库中所有的表都必须映射到相应的实体Bean。如果一个表并不表示一个商业实体,或者它存储的信息是某个商业实体的部分信息,那么它只需要实现为一个辅助类(这样做的好处是显而易见的,参考《J2EE核心模式》)。例如一个在线商店的应用程序中,用户提交的每一个订单都有多行明细条目。该应用程序中存储订单和订单条目信息的数据表如下图5-2:

图 5-2 一对多关系:订单和订单条目

订单条目不仅从属于订单,而且它的存在也依赖于订单的存在。所以lineitems表应该被表示成一个辅助类而不是一个实体Bean。当然在这里用辅助并不是必需的,但是它可以提高性能,因为一个辅助类比实体Bean使用的资源要少。(《J2EE核心模式》一书关于辅助类有详细描述)。

下面讲的例子的源文件在j2eetutorial/examples/src/ejb/order目录下。在j2eetutorial/examples目录下执行ant order命令可以编译它们。一个样本OrderApp.ear文件存放在目录下。

这个例子用LineItem和OrderBean两个类展示了如何用辅助类实现一对多关系。LineItem类的数据成员对应lineitems表中的列,itemNo表示lienitems表的主键,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类有一个ArrayList类型的数据成员lineItems,lienItems里的每一个元素都是一个LineItem对象。lineItems变量通过OrderBean的ejbCreate方法传入,该方法先向orders表里插入一行,然后为lineItems里的每一个LineItem对象向lineitems表中插入一行。下面是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客户端程序创建一个ArrayList,并用LineItem对象填充它。在调用create方法是将它传给实体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方法就更不用说了,它们必须访问两个表以保证所有的数据跟数据库中是同步的。

客户端可以通过调用ejbFindByProductId方法来查找包括特定产品的所有订单。这个方法查询lineitems表中特定productId值确定的所有纪录,并返回一个Order对象的集合。OrderClient客户端程序遍历集合并在屏幕上打印出每个订单的主键值:

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. 创建用到的表

a) 进入j2eetutorial/examples directory.目录

b) 执行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。

将子表实现为实体Bean

在一下这些情况下你可以将一个子表实现为实体Bean:

☆ 子表存储的信息并不依赖于父表

☆ 子表代表的商业实体可以独立于父表存在

☆ 其它应用程序可能不访问父表而只访问子表(考虑可重用性)

考虑下面的场景:一个公司里,一个销售代表负责为很多客户服务,而一个客户只跟一个销售代表联系。公司用一个数据库应用程序跟踪销售代表的业绩。数据库需要两张表,父表salsrep和子表cunstomer,salesrep表中的一行对应customer表中的多行。它们的关系如图5-3:

图 5-3 一对多关系:销售代表和客户

实体Bean类SalesRepBean和CunstomerBean实现了sales表和customer表之间的一对多关系。

这个例子的源文件在j2eetutorial/examples/src/ejb/salesrep目录下,要编译它们请在j2eetutorial/examples目录下执行antsalesrep命令。样本文件SalesRepApp.ear存放在j2eetutorial/examples/ears directory目录下。

SalesRepBean类有一个数据成员customerIds,它是一个String对象填充的ArrayList,这些String元素代表跟特定销售代表联系的客户。因为customerIds反映了两个实体之间的关系,所以SalesRepBean必须保证它们是最新的可用数据(如果某个客户和销售代表的关系改变而customerIds并没有变,那么根据它度出来的数据就是错的)。

SalesRepBean类在setEntityContext方法而不是ejbCreate方法中给customerIds一个实例对象,容器只在创建实体Bean实例时调用setEntityContext方法一次以保证只对customerIds实例化一次。因为在实体Bean的生命周期中一个实体Bean实例可以有不同的身份(主键被重新赋值),在ejbCreate方法中实例化customerIds会造成多次不必要的实例化。因此SalesRepBean类的在setEnetityContext方法中实例化customerIds:

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方法的办法:从数据库中读出或者从CustomerEJB实体Bean得到。第一种方法会比第二肿快,但是会使SalesRepBean类的代码依赖于CustomerEJB的底层数据表的实现细节(何种数据库,字段数量等等)。以后可能你想要修改CustomerEJB对应的表(把应用程序移植到另外的J2EE平台服务器上),这时需要修改SalesRepBean类的代码。但是从CustomerEJB实体Bean得到数据就没有这些问题了。这两种方法体现了应用程序中如何在性能和可移植性之间平衡考虑。SalesRepEJB选择了可移植性调用CustomerEJB的findSalesByRep和getPrimaryKey方法来获得cunstomerIds的值(可以用DAO模式来解决这个冲突,见《J2EE核心模式》):

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的setSalsRepId方法来更新数据库。SalesRepBean下一个商业方法调用时,loadCustomerIds方法因ejbLoad方法被调用而被调用来更新customerIds数据成员。(为保证ejbLoad方法在每个商业方法前被调用,把所有商业方法的事物属性设置为:Required。)例如:SalesRepClient客户端程序将客户Mary Jackson的销售代表改变的代码如下:

Customer mary = customerHome.findByPrimaryKey("987");

mary.setSalesRepId("543");

543是销售代表Janice Martin的salesRepId值。要列出Janice的所有客户,客户端调用getCunstomerIds方法,然后遍历返回的集合对每一个集合中得到的CustomerEJB实体Bean实例调用findByPrimaryKey方法:

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 directory目录

b) 执行命令: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

多对多关系

对于多对多关系,每个实体都对应关系另一方的多个实体。例如学生选课的例子,每门课程都有很多学生来上,而每个学生也不可能只上一门课,在数据库中,这种关系被表示成一个由外键(关系中的两张表的主键)组成的关系引用表。图5-4表enrollment是一个关系引用表。这些表分别被StudentBean、CustomerBean和EnrollerBean类访问。

图 5-4 多队多关系:学生和课程

这个例子的源文件在j2eetutorial/examples/src/ejb/enroller目录下,要编译这些源文件在j2eetutorial/examples目录下执行ant enroller命令。样本文件EnrollerApp.ear方在j2eetutorial/examples/ears目录下。

StudentBean和CourseBean类都有一个以外键对象为元素的ArrayList数据成员。StudentBean类中是:courseIds,记录学生参加的课程。CourseBean类中是studentIds。

StudentBean类的ejbLoad方法调用私有方法loadCourseIds将课程元素加入ArrayList中,后者从EnrollerEJB会话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());

   }

}

被上面方法调用的EnrollerBean类的getCourseIds方法直接查询数据库:

select courseid from enrollment

where studentid = ?

只有EnrollerBean类访问enrollment表,因此这各类管理着学生-课程之间的关系。当一个学生选择一门课,客户端调用enroll商业方法,在enrollment表中插入一行:

insert into enrollment

values (studentid, courseid)

如果学生退选一门课,则调用unEnroll方法删除一行:

delete from enrollment

where studentid = ? and courseid = ?

如果一个学生离开了学校,则调用deleteStudent方法删除和该学生相关的所有纪录:

delete from enrollment

where student = ?

EnrollerBean类不删除student表中的对应纪录,这个工作由StudentBean类的ejbRemove方法来完成。为了保证这两个表的删除作为一个单一操作完成,它们必须在同一个事务中。关于事务参考14章。

运行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

四.BMP的主键

在部署实体Bean时,你必须在部署描述符指定主键类型。很多时候,主键类型可能是String、Integer或者其他J2SE或J2EE标准库中的类。但是有很多实体Bean的主键是复合字段,你必须自己定义主键类。

主键类

下面的主键类是一个复合组建类,productId和vendorId字段共同标志一个唯一的实体:

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();

   }

}

对BMP,主键类规则如下:

1. 主键类必须是公有(public)类

2. 所有数据成员都是共有(public)的

3. 有一个共有(public)的缺省构造函数

4. 重载hashCode()和equals(Object other)方法

5. 可序列化

实体Bean类中的主键

在BMP中,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方法得到实体Bean得主键值:

SavingsAccount account;

...

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

实体Bean调用EntityContext对象的getPrimaryKey方法找回自己的主键值:

EntityContext context;

...

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

五.异常处理

企业Bean抛出的异常有两类:系统异常和应用程序异常。

系统异常是指应用程序的支撑服务出现异常。这样的异常例子有:不能得到数据库连接,数据库满造成SQL语句插入失败,一个lookup方法找不到需要的对象等等。如果企业Bean遇到一个系统级的问题,它将抛出javax.ejb.EJBException,容器会把该异常包装到一个RemoteException异常中返回给客户端。因为EJBException是RutimeException的子类,所以你不需要将它列在throws子句中。当一个系统异常发生时,EJB容器可能会销毁企业Bean实例,因此系统异常不能被客户端处理,它需要系统管理员的干涉。

应用程序异常表示一个企业Bean的动作违反特定的商业逻辑。应用程序异常又分为两类:自定义异常和系统预定异常。自定义异常是自己编码的表示商业逻辑违例的异常类,例如SavingsAccountEJB例子的debit方法抛出的InsufficentBalanceException异常。同时javax.ejb包还为处理一些通用问题预定义了很多异常。例如ejbCreate方法在输入参输非法的情况下抛出的CreateException异常。容器不对企业Bean抛出的应用程序异常做手脚,客户端可以处理捕获的任何应用程序异常。

如果在事务中出现系统异常,EJB容器将回滚该事务。然而容器不为应用程序异常回滚事务。

表5-2列出了javax.ejb中定义的异常,除了NoSuchEntityException和EJBException是系统异常,其余的都是应用程序异常。

表5-2 异常
方法 抛出异常 异常原因
ejbCreate CreateExcetption 非法参数
EjbFindByPrimaryKey(所有返回单个对象的查找方法) ObjectNotFoundException(FinderException的子类) 被查询的实体在数据库中不存在
ejbRemove RemoveException 从数据库中删除对应实体数据失败
ejbLoad NoSuchEntityException 要查找的记录数据库中没有
ejbStore NoSuchEntityException 要更新的实体数据数据库中没有
所有方法 EJBException 出现系统异常