1、J2EE 应用程序 BMP 实现实体 Bean 的编码技术数据是大多数商业应用程序的核心(这个好像是废话,没有数据程序也只能玩玩了) 。在 J2EE 应用程序中,实体 Bean 表示存储在数据库中的商业实体。如果用 BMP 实现实体 Bean,你必须自己编写数据库访问代码。虽然写这些代码是附加的责任,但是因此你可以更灵活的控制实体 Bean 访问数据库的行为(当然这得你自己愿意才行) 。本章讨论 BMP 实现实体 Bean 的编码技术。内容:1SavingsAccountEJB实体 Bean 类Home 接口Remote 接口运行该例子2用 deploytool 部署 BMP 实现的实体 Be
2、an3为 BMP 映射表间关系一对一关系一对多关系多对多关系4BMP 的主键主键类实体 Bean 中的主键获取主键5处理异常一 SavingsAccountEJB本例中的实体 Bean 表示一个样本银行账号。SavingsAccountEJB 的状态信息保存在一个关系数据库的 savingaccount 表中。创建该表的 SQL 语句如下:CREATE TABLE savingsaccount(id VARCHAR(3) CONSTRAINT pk_savingsaccount PRIMARY KEY,firstname VARCHAR(24),lastname VARCHAR(24),bal
3、ance NUMERIC(10,2);SavingsAccountEJB 由三个文件组成: 实体 Bean 类(SavingsAccountBean) Home 接口(SavingsAccountHome) Remote 接口(SavingsAccount)本例应用程序还包括下面的两个类: 一个异常类 InsufficientBalanceException 一个客户端类 SavingsAccountClient该例子的源代码文件在 j2eetutorial/examples/src/ejb/savingsaccount 目录下,可以在 j2eetutorial/examples 目录下用 a
4、nt savingsaccount 来编译这些代码。一个样本 SavingsAccountApp.ear 文件放在 j2eetutorial/examples/ears 目录下。实体 Bean 类( SavingsAccountBean)SavingsAccount 是本例中的实体 Bean 类。从它的代码可以看出 BMP 实现实体 Bean 的基本要求。首先实现一下接口和方法: EntityBean 接口 大于等于零对的 ejbCreate 和 ejbPostCreate 方法(实体 Bean 可以没有 ejbCreate 方法,只有查找方法,但是不能两者都没有) 查找(Finder)方法
5、商业方法 Home 方法(这里的 Home 方法不好理解,如果是指生命周期方法,应该包含ejbcreate 等方法)另外它还有如下的一些特征: 该类访问属性为 public 该类不可以被定义为 abstract 或者 final 该类包含一个空构造函数 该类不可以实现 finalize 方法EntityBean 接口该接口也继承至 EnterpriseBean 接口(EnterpriseBean 接口是 SessionBean 和 EntityBean共同的父接口,它继承至 Serializable 接口,没有任何方法) 。EntityBean 定义一些方法,如 ejbActive、ejbLo
6、ad 和 ejbStore 等等,你必须在实体 Bean 类里实现它们。EntityBean接口的定义如下:package javax.ejb;import java.rmi.RemoteException;/ Referenced classes of package javax.ejb:/ EnterpriseBean, EJBException, RemoveException, EntityContextpublic interface EntityBeanextends EnterpriseBeanpublic abstract void setEntityContext(Entit
7、yContext 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;publi
8、c 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 方法完成如下工作: 将实体状态(表述实体
9、 Bean 的属性字段)插入数据库 初始化实例变量,就是对实体 Bean 的属性字段赋值 返回主键本例中 SavingsAccountBean 的 ejbCreate 方法调用私有方法 insertRow 来将实体状态插入数据库,insertRow 方法向数据库发出一条 INSERT 的 SQL 命令。下面是 ejbCreate方法的代码:public String ejbCreate(String id, String firstName, String lastName, BigDecimal balance)throws CreateException if (balance.signu
10、m() = -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;retur
11、n id;虽然 SavingsAccountBean 类只有一个 ejbCreate 方法,但是一个企业 Bean 可以有多个ejbCreate 方法。例如前一章 CartEJB 的例子。编写实体 Bean 的 ejbCreate 方法许要遵循如下规则: 访问权修饰符必须是 public 返回值类型必须是主键类 参数类型必须符合 RMI 调用规则 该方法不可以声明为 final 或者 staticthrows 子句要包含 javax.ejb.CreateException 异常,通常遇到非法参数 ejbCreate 方法会抛出该异常。如果 ejbCreate 方法因为存在使用相同主键的其他实体
12、而无法创建实体,将抛出 javax.ejb.DuplicateKeyException 异常,它是 CreateException 的子类。如果客户端捕获 CreateException 或者 DuplicateKeyException 异常,就表示实体创建失败。实体 Bean 的状态数据可能被 J2EE 服务器不知道的其他应用程序直接插入到数据库里。例如,你可以直接用 SQL 语句在数据库工具中插入一行到 savingsaccount 表里。虽然该行对应的实体 Bean 没有被 ejbCreate 方法创建,但客户端仍然可以找到该行对应的实体 Bean(不用说,这里当然是用 ejbFinde
13、r 方法了,下面会介绍) 。EjbPostCreate 方法对每一个 ejbCreate 方法,你都必须在实体 Bean 中写一个对应的 ejbPostCreate 方法。因为 EJB 容器在调用 ejbCreate 方法后接着就调用 ejbPostCreate 方法。跟 ejbCreate 方法不同的是,ejbPostCreate 方法可以调用 EntityContext 接口的 getPrimaryKey 和getEJBObject 方法(在前一章的传递企业 Bean 对象的引用一节讨论过) 。EjbPostCreate 方法大部分情况下什么事也不干。EjbPostCreate 方法声明必
14、须符合一下要求: 参数数量类型声明顺序必须跟对应的 ejbCreate 方法相同 必须声明为 public 不能声明为 final 或者 static 返回值必须是 voidthrows 子句要包含 javax.ejb.CreateException 异常。ejbRemove 方法客户端调用 remove 方法来删除实体 Bean。该调用会引发 EJB 容器调用 ejbRemove 方法,ejbRemove 方法从数据库中删除实体状态对应的数据。SavingsAccountBean 的ejbRemove 方法调用 deleteRow 私有方法向数据库发送一条 DELETE 的 SQL 命令。代
15、码如下: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 状态数据对应的行时,
16、实体 Bean 也会被删除。ejbLoad 和 ejbStore 方法如果 EJB 容器要同步实体 Bean 的属性子段和数据库中对应的数据就调用这两个方法。顾名思义,ejbLoad 方法从数据库中取出数据并刷新属性子段的值,ejbStore 方法把属性子段值写入数据库。客户端不能调用这两个方法。如果商业方法在一个事务环境中执行,EJB 容器会在该方法执行前调用 ejbLoad,并在该方法执行完后立即调用 ejbStore 方法。这样你不必在商业方法中调用这两个方法来刷新和存储实体 Bean 数据。SavingsAccountBean 依赖容器同步实体和数据库的数据,所以商业方法应该在事务环境
17、中执行。如果 ejbLoad 和 ejbStore 方法不能在数据库中找到实体的数据就会抛出javax.ejb.NosuchEntityException 异常,它是 EJBException 的子类。因为它的父类是RutimeException 的子类,所以你不必将该异常加到 throws 子句中。该异常在返回客户端前被容器封装进 RemoteException 的一个实例。在 SavingsAccountBean 类中, ejbLoad 调用 LoadRow 私有方法,后者向数据库发送一条 SELECT 的 SQL 命令并将读出的数据赋给 SavingsAccountBean 的属性字段。
18、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
19、.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 中
20、对应上面 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;查找方法的实现细节用应用程序决定,例如
21、上例的 ejbFindByLastName 和ejbFindInRange 方法的命名都是很随意的。但是 ejbFindByPrimaryKey 方法命名不能随意,就像它的名字所暗示的,该方法有一个用来查找实体 Bean 的主键类型的参数。SavingsAccountBean 类中,主键是 id 子段。下面是 SavingsAccountBean 的ejbFindByPrimaryKey 方法的实现代码:public String ejbFindByPrimaryKey(String primaryKey) throws FinderException boolean result;try r
22、esult = 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.“);上面的实现也许对你来说有点陌生,因为它的参数和返回值都是主键类。不过,记住客户端并不直接调用 ejbFindByPrima
23、ryKey 方法,而是由 EJB 容器代劳的。客户端只能调用在 Home 接口中声明的 findByPrimaryKey 方法。下面总结一下 BMP 实现实体 Bean 的查找方法的规则: 必须实现 ejbFindByPrimaryKey 方法 方法名必须用 ejbFind 做前缀 方法不能用 final 或者 static 方法 如果 BMP 实现 Remote 接口组中的方法,则方法的参数和返回值类型必须是符合RMI API 调用的合法类型 返回类型必须是主键类或者主键类的集合Throws 字句要包括 javax.ejb.FinderException。如果查找方法返回一个主键类对象但是被
24、查找的实体不存在,该方法将抛出 javax.ejb.ObjectNotFoundException,该异常是 javax.ejb.FinderException 的子类。如果查找方法返回主键类对象集合,而且也没有符合要求的实体,则该方法返回空集合。商业方法商业方法处理你想封装在实体 Bean 中的商业逻辑。一般商业方法并不访问数据库,以使你可以把商业逻辑和数据库访问代码分离。SavingsAccountBean 包含以下一些商业方法:public void debit(BigDecimal amount) throws InsufficientBalanceException if (pare
25、To(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;Sav
26、ingsAccountClient 客户端这样吊用这些商业方法: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 的商业方法的签名规则是相同的: 方法名不能和 EJ
27、B 体系定义的方法名冲突,比如商业方法不能命名为:ejbCreate或者 ejbActivate 访问修饰必须是 public Remote 接口租中定义的方法的参数和返回值必须是 RMI API 规范的合法类型Throws 子句没有特殊要求。例如 debit 方法抛出 InsufficientBalaceException 自定义异常。为了捕获系统异常,商业方法可以抛出 javax.ejb.EJBException。Home 方法Home 方法包含那些应用于一个特定企业 Bean 类的所有实例的商业逻辑。正好和只应用于由主键标志的企业 Bean 单个实例的商业方法相反。Home 方法调用期间
28、,企业Bean 实例不仅没有唯一的标志主键,也不具备表示一个商业实体的状态。因此,Home 方法不可以访问企业 Bean 的持久性字段(企业 Bean 中的实例变量) 。 (对于CMP,Home 方法也不能访问关系字段。 )Home 方法查找到企业 Bean 的实例的集合然后通过集合的迭代器调用商业方法是实现Home 方法的典型做法。SavingsAccountBean 类的 ejbHomeChargeForLowBalance 就是这样做的。该方法对余额少于某个特定数值的账户收取服务费,首先调用 findInRange方法得到符合条件所有的账户,它返回一个 SavingAccount 实现类
29、实例的集合,然后通过集合的迭代器访问这些远程接口对象,在检查了余额之后调用 debit 商业方法收取费用。下面是代码:public void ejbHomeChargeForLowBalance(BigDecimal minimumBalance, BigDecimal charge) throws InsufficientBalanceException try SavingsAccountHome home =(SavingsAccountHome)context.getEJBHome();Collection c = home.findInRange(new BigDecimal(“0.
30、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.ge
31、tMessage(); 该方法在 Home 接口中对应的定义为 chargeForLowBalance(稍后的 Home 方法定义将具体介绍) 。客户端可以通过该接口访问 Home 方法:SavingsAccountHome home;.home.chargeForLowBalance(new BigDecimal(“10.00“), new BigDecimal(“1.00“);综上所述,实体 Bean 类中 Home 方法的实现要遵循以下规则: 方法名必须以 ejbHome 开头 访问修饰符必须是 public 方法不可以是 static 方法根据应用程序逻辑确定 throws 子句,th
32、rows 子句中不能包含java.rmi.RemoteException 异常。数据库访问表 5-1 列出了 SavingsAccountBean 的数据库访问操作。商业方法并不在表中,因为它们不访问数据库,它们只是更新企业 Bean 的持久性子段,这些字段的值会在 EJB 容器调用 ejbStore 方法时被写入数据库。但是这并不是强制的规则,你也可以在商业方法中直接访问数据库来存取数据,这要根据你的应用程序的具体需要来决定。访问数据库前你必须先得到数据库的连接,关于数据库连接的更多信息将在 16 章讨论。表 5-1 SavingsAccountBean 中的 SQL 语句 方法 SQL 语
33、句ejbCreate INSERTejbFindByPrimaryKey SELECTejbFindByLastName SELECTejbFindInRange SELECTejbLoad SELECTejbRemove DELETEejbStore UPDATEHome 接口Home 接口定义了让客户端创建和查找实体 Bean 的方法。本例中 SavingsAccountHome接口的实现如下:import java.util.Collection;import java.math.BigDecimal;import java.rmi.RemoteException;import java
34、x.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 Collectio
35、n 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, RemoteExcep
36、tion;定义 create 方法create 方法的定义规则: 必须和企业 Bean 类中对应的 ejbCreate 方法有相同的参数数量、类型和排列顺序。就是要为每个 Home 中的定义的可用 create 方法在企业 Bean 中实现对应的ejbCreate 方法。 返回企业 Bean 的远程接口类型 throws 子句包括对应的 ejbCreate 方法和 ejbPostCreate 方法的 throws 子句中出现的所有异常,另外还要包括 javax.ejb.CreateException 如果方法是在 Home 接口而不是在 LocalHome 接口中定义的则 throws 子句必
37、须包括 java.rmi.RemoteException 异常定义查找方法跟 create 的规则相似,Home 接口中的每个查找方法都对应一个企业 Bean 类中的一个查找方法。Home 接口中的查找方法必须以 find 开头,企业 Bean 类中对应的方法以ejbFind 开头。本例中 SavingsAccountHome 接口定义的 findByLastName 对应的SavingsAccountBean 类的方法为 ejbFindByLastName 方法。总结一下 Home 接口中的方法必须符合一下条件: 参数个数类型和顺序必须和对应的 ejbFind 方法相同 返回实体 Bean
38、远程接口类型,或是远程接口类型的集合 throws 字据除了包括对应的 ejbFind 方法的 throws 子句中出现的异常外,还要包括 javax.ejb.FinderException 不是在 Local Home 接口中定义的方法 throws 子句还要包括java.rmi.RemoteExceotion 异常定义 Home 方法Home 接口中的每个 Home 方法都在实体 Bean 类中有一个对应的方法,这些方法的命名没有特殊的前缀,相反对应的实体 Bean 类中的方法要以 ejbHome 开头。如本例中SavingsAccountBean 类中前面提到的 ejbHomeCharg
39、eForLowBalance 方法,在 Home 接口中的对应方法名为 chargeForLowBalance。除了 Home 方法不抛出 FinderException,它的签名规则和查找方法是一样的。Remote 接口Remote 接口继承 javax.ejb.EJBObject 接口,定义远程客户端访问的商业方法。本例中 SavingsAccount 远程接口定义如下:import javax.ejb.EJBObject;import java.rmi.RemoteException;import java.math.BigDecimal;public interface Savings
40、Account 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 getBal
41、ance()throws RemoteException;会话 Bean 和实体 Bean 的远程方法的定义规则是相同的,这里再重复一下: 每个方法在企业 Bean 类中都必须有对应的方法 方法签名必须和企业 Bean 类中的对应方法相同 参数和返回值必须是符合 RMI 规范的类型 throws 子句企业 Bean 类对应方法的 throws 子句的基础上添加java.rmi.RemoteException而 Local 接口有所不同: 参数和返回值不需要是 RMI 合法类型 throws 子句不需要包括 java.rmi.RemoteException运行本例子配置数据库本例使用 Cloun
42、dscape 数据库,该数据库软件被包括在 J2EE SDK 包里。1. 启动数据库。在命令方式下执行如下命令cloudscape start关闭命令为:cloudscape -stop2. 创建 savingsaccount 表a) 进入 j2eetutorial/examples 目录b) 执行 ant create-savingsaccount-table 命令你也可以用其他数据库来运行本例(要是 J2EE 服务器支持的数据库) 。在其他数据库中创建该表要执行 j2eetutorial/examples/sql/savingsaccount.sql 脚本文件。部署应用程序1 用 depl
43、oy 工具打开 j2eetutorial/examples/ears/SavingsAccountApp.ear 文件2 执行 ToolsDeploy 菜单命令部署。确定 Introduction 对话中你选中了 Return Client JAR 复选框运行客户端1 在命令方式下进入 j2eetutorial/examples/ears 目录2 设置 APPPATH 环境变量为 SavingsAccountAppClient.jar 所在目录3 执行以下命令(只有一条命令,有点长):runclient -client SavingsAccountApp.ear -name SavingsAc
44、countClient textauth4 在登录提示符下输入用户名 guest,密码 guest1235 这一步什么也不做,看看结果:balance = 68.25balance = 32.55456: 44.77730: 19.54268: 100.07836: 32.55456: 44.774.007.00二用 deploytool 部署 BMP 实现的实体 Bean第 4 章介绍了创建一个会话 Bean 包的步骤,创建实体 Bean 包的步骤相似,但是有以下不同:1. 在新建企业 Bean 向导(New Enterprise Bean)中,指定企业 Bean 类型和持久性管理类型a)
45、在 General 对话框中,选定 Entity 单选项b) 在 Entity Settings 对话框中,选定 Bean-Managed Persistence2. 在 Resource Refs 页,指定企业 Bean 引用的资源工厂。这些设置使企业 Bean 可以访问数据库。具体的设置信息参考用 deploytool 工具配置资源引用一节3. 部署前,检查你的 JNDI 名是否都是对的a) 从树中选择要部署的应用程序b) 选择 JNDI Names 页三为 BMP 映射表间关系在关系数据库中,数据表可以通过共同的列建立关系。数据库的关系影响了他们对应的实体 Bean 的设计。本节分以下几
46、类讨论实体 Bean 如何映射数据库中的表间关系 一对一 一对多 多对多一对一关系一对一关系中一个表 1 的一行数据只对应于表 2 的一行数据,表 2 的一行数据也只对应表 1 的一行数据。例如:一个仓库应用程序中,储藏箱(storagebin)表和小物件(widget)表就可能是一对一关系。这个应用程序要为物理仓库中每个储藏箱只装一个小物件并且每个小物件也只能装在一个储藏箱中建立逻辑模型。图 5-1 说明了两个表的关系。因为 storagebinid 字段可以唯一确定 storagebin 表中的一行,它是这个表的主键。widgetid 是 widget 表的主键。storagebin 表也
47、有一个widgetid 字段来关联两张表的数据。通过在 storagebin 表中引用 widget 表的主键,可以确定一个物件存储在仓库中的哪个储存箱中。因为 storagebin 中 widget 字段引用其他表的主键,所以它是一个外键。 (本章的图例中用 PK 表示主键,FK 表示外键。)图 5-1 一对一关系 一般子表包含一个匹配父表数据的外键。子表 storagebin 中的外键 widgetid 的值依赖于父表 widget 的主键值。如果 storagebin 表中有一行的 widgetid 值为 344,那么widget 表中一定有一行数据的 widgetid 也是 344。在
48、设计数据库应用程序时,你必须保证子表和父表之间的依赖关系正确无误。有两种方法可以实现这种保证:在数据库中定义参照约束或者在应用程序中编码检查。本例中 storagebin 表定义了一个参照约束 fk_widgetid:CREATE TABLE storagebin(storagebinid VARCHAR(3) CONSTRAINT pk_storagebin PRIMARY KEY,widgetid VARCHAR(3),quantity INTEGER,CONSTRAINT fk_widgetidFOREIGN KEY (widgetid)REFERENCES widget(widgeti
49、d);下面讲到的例子的源文件可以在 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 值匹配的 storageBinI