44.5. Bean管理持久性的Entity Bean

44.5.1. 概述

对存储于数据库或者传统系统中的业务实体,在基于J2EE™平台的企业应用开发中,通常使用Entity Bean来提供业务实体的对象视图,持久性指Entity Bean实例和底层数据存储之间进行状态交换的数据访问协议。

在EJB2.1规范中,按照持久性的管理机制,划分了两种类型的Entity Bean。即容器管理持久性的Entity Bean和Bean管理持久性的Entity Bean。

Bean管理持久性的Entity Bean中,可以在组件类或其他辅助类对Bean的持久性进行编码。对于客户端而言,表示业务实体的Entity Bean不是底层数据存储中数据的简单表示,而是提供了面向对象特征的,具有访问接口、内部状态和封装机制的对象。

44.5.2. Bean管理持久性的Entity Bean

Bean管理持久性的Entity Bean具有Enterprise JavaBean的共有特征,如包含组件类、组件接口、Home接口等。不同的是,由于这种组件模型必须依靠开发者提供对持久性管理的实现,因此,Bean管理持久性的Entity Bean的组件模型有一些特别的方面。

从前面的第 44.4 节 “Entity Bean”中,可以了解到,组件接口是开发者暴露给客户端的、对实例的访问方法,Home接口提供客户端创建、清除实例的方法,组件类中提供对定义在以上两个接口中方法的实现。对于Bean管理持久性的Entity Bean,这些定义在接口中的方法决定了开发者需要实现的数据访问和同步等等持久性方面的能力。

下面 ,我们从Bean实例的创建、状态装载与保存、实例的清除等方面,来了解Bean管理持久性的Entity Bean的特点。

44.5.2.1. 打开与释放资源

由前面的第 44.4 节 “Entity Bean”中可知,Entity Bean实例从不存在状态到实例池状态时,容器将调用组件类的setEntityContext方法,在实例将容器被清除,由实例池返回到不存在的状态时,容器将调用组件类的unsetEntityContext方法。由于Bean管理持久性的Entity Bean通常需要对持久存储进行访问,需要占用一些资源连接,因此,开发者通常需要在这两个方法中对资源(如数据库连接)进行打开和释放的操作。

44.5.2.2. 实例的创建

Entity Bean既然表示底层数据存储中的业务实体,Bean实例的创建意味着底层数据存储中数据的创建。从EJB的组件模型中,可以了解到,实例的创建通常是客户端调用组件Home接口中的create方法,此方法被传播到容器,容器随之调用组件类中对应的ejbCreate方法,客户端调用create方法使用的参数被容器用于调用组件的ejbCreate方法,因此,开发者在使用ejbCreate方法参数初始化实例时,也需要在底层数据存储中创建数据,如通过JDBC、连接器(Connector)等方式。

因此,开发者需要在ejbCreate方法中直接在数据存储中创建数据或者通过其他的辅助类进行,在数据创建之后,开发者需要编码返回实例的Primary Key。然后,容器返回实例的组件接口引用到客户端。

44.5.2.3. 实例状态与持久存储

在实例的生存周期之内,客户端可通过组件接口改变实例的状态,因此,开发者需要提供同步实例状态与持久存储的实现。

从前面Entity Bean的介绍中,对于Bean管理持久性的Entity Bean组件类,实例的状态与持久存储数据的同步,是通过容器对定义在组件类接口中的方法进行调用完成的,即javax.ejb.EntityBean接口中的ejbStore和ejbLoad方法,因此,开发者必须在这两个方法中提供对实例状态数据的装载和保存的实现。

44.5.2.4. 实例的清除

对于Bean管理持久性的Entity Bean,如同其他类型的EJB组件,客户端可以通过调用组件接口中和组件Home接口中的remove方法,清除实例。当客户端调用remove方法,容器调用组件类中的ejbRemove方法。开发者需要在ejbRemove方法中对持久存储进行访问,删除实例的状态。

44.5.3. 必须遵守的规则与范例

本节以一个账户的例子演示Bean管理持久性的Entity Bean的编写。使用如下的命名约定:

  • 组件类以“<实体名>EJB” 的方式命名,如SavingsAccountEJB;

  • 组件的Home接口以“<实体名>Home”的方式命名,如SavingsAccountHome;

  • 组件的组件接口以“<实体名>”的方式命名,如SavingsAccount;

本例中的业务实体对应的表模型根据如下SQL语句创建:

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

如采用不同类型的数据库,上面的SQL语句可能会不同。上面语句经验证可用于SQL Server2000,Oracle8.16 for windows。

44.5.3.1. 组件类

开发者必须遵循如下规则:

  • 实现javax.ejb.EntityBean接口;

  • 零个到多个ejbCreate和对应的ejbPostCreate方法;

  • 编写finder方法;

  • 编写业务方法;

  • 编写Home方法;

  • 组件类必须声明为公有的(public);

  • 组件类不能被定义为抽象的(abstract)和静态的(static);

  • 必须包含一个无参数的公有构建器(constructor);

  • 不能实现finalize方法;

44.5.3.1.1. ejbCreate方法

ejbCreate方法中,开发者需要进行插入实体状态到数据库、初始化实例变量以及返回Primary Key等操作。

本例中的ejbCreate方法调用了类中的私有insertRow方法,此方法执行一个SQL中的INSERT语句,将实例状态插入到数据库,如下:

...
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;
}
...

ejbCreate方法必须遵循如下规则:

  • 方法签名必须为public;

  • 返回值类型必须是Primary Key类型;

  • 参数必须是合法的RMI类型;

  • 方法不能声明为final或static;

  • 方法必须声明抛出javax.ejb.CreateException异常;

44.5.3.1.2. ejbPostCreate方法

对于每个在组件类中定义的ejbCreate方法,必须有一个对应的ejbPostCreate方法。容器将在ejbCreate方法调用完成后,立即调用ejbPostCreate方法,由于在ejbPostCreate方法内部,可以调用EntityContext接口的getPrimaryKey和getEJBObject方法。

ejbPostCreate的方法签名必须遵循如下规则:

  • 参数数量与类型必须与对应的ejbCreate方法一致;

  • 访问控制签名必须是公有的(public);

  • 方法签名不能是final或static;

  • 返回值类型必须为void;

通常情况下,ejbPostCreate方法为空。

44.5.3.1.3. ejbRemove方法

容器在清除组件实例之前会调用ejbRemove方法。本例中的ejbRemove方法调用了组件类中的deleteRow方法,从数据库中清除组件实例得状态,如下:

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

组件类中必须提供对Home接口中定义的每个finder方法的实现,必须遵循如下规则:

  • 组件类中必须定义ejbFindByPrimaryKey方法;

  • finder方法必须以“ejbFind”前缀开始;

  • 方法必须被定义为public;

  • 方法不能被声明为static或final;

  • 对于提供远程Home接口的Entity Bean,方法的参数类型和返回值必须是合法的RMI类型;

  • 返回值必须是Primary Key类型,或是包含Primary Key的集合(Collection);

  • 抛出语句必须包含javax.ejb.FinderException异常的抛出;

下面是范例中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.");	
   }	
}
...
44.5.3.1.5. 业务方法

通常情况下,业务方法不能直接对数据库进行访问,只包含必需的业务逻辑。业务方法的编写必须遵循如下规则:

  • 不能与组件类中定义的其他方法的命名方式冲突,如“ejbCreate” 等;

  • 方法签名必需被声明为public;

  • 方法不能被声明为static或final;

  • 对于提供远程访问接口的Entity Bean,方法的参数类型和返回值必须是合法的RMI类型

以下代码片断是一个业务方法的范例:

...
public void debit(BigDecimal amount) 	
   throws InsufficientBalanceException {	
	
   if (balance.compareTo(amount) == -1) {	
       throw new InsufficientBalanceException();	
   }	
   balance = balance.subtract(amount);	
}
...
44.5.3.1.6. Home方法

Home方法用于对一个Entity Bean的所有或部分实例进行操作的方法。一般,Home方法对Entity Bean实例的集合进行定位,然后依次调用实例的业务方法,对之进行操作。如下例:

...
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方法必须遵循如下规则进行编写:

  • 方法必须以“ejbHome ”前缀开头;

  • 方法必须被声明为public;

  • 方法不能被声明为static;

  • 不能声明抛出java.rmi.RemoteException异常;

44.5.3.2. Home接口

Home接口的编写,其规则参考第 44.4.4.1 节 “Home接口”中的描述,本节中的范例代码如下:

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;	
}

44.5.3.3. 组件接口

组件接口编写的规则,参考第 44.4.4.3 节 “组件接口”中的描述,本节中的范例代码如下:

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;	
}

44.5.4. 异常处理

通常把Entity Bean抛出的异常分为两个类别:系统级异常和应用级异常。

44.5.4.1. 系统级异常

系统级异常的抛出通常用于指明对应用提供支持的服务产生的问题,如:不能获得数据库连接、由于数据库原因不能插入数据或lookup方法查找不到对象等等。对于此类问题,开发者应该抛出javax.ejb.EJBException异常,容器将封装此异常到一个java.rmi.RemoteException并返回到客户端。因javax.ejb.EJBException是java.lang.RuntimeException的子类,因此,在方法声明中,不需要在throws子句中进行声明。而且,如果一个Entity Bean实例抛出了一个系统级的异常,客户端并不具有对异常的处理能力,只能由系统管理员对应用进行调整。

44.5.4.2. 应用级异常

应用级异常指应用的业务逻辑发生错误。包含两种类型,定制的应用级异常和预定义的应用级异常。定制的应用级异常通常由开发者定义,与业务逻辑紧密关联,如某个本节中的SavingsAccountEJB中的debit方法抛出的InsufficientBalanceException异常。另外,EJB的编程模型中还包括一些预定义的异常类型,如javax.ejb.CreateException、javax.ejb.FinderException异常等等。

44.5.4.3. 组件异常类型

[注意]注意

如果某个事务边界中的Entity Bean实例的方法抛出了系统级异常,容器将回滚事务,如抛出应用级别的异常,容器将不会回滚事务。

下表包含Entity Bean的组件模型API中定义的异常类型的简单介绍,除javax.ejb.EJBException和javax.ejb.NoSuchEntityException之外,都属应用级异常:

方法名抛出异常原因
ejbCreatejavax.ejb.CreateException输入参数无效
ejbFindByPrimaryKeyjavax.ejb.FinderException数据库中不存在需要查找的数据
ejbRemovejavax.ejb.RemoveException不能从数据库中删除数据
ejbLoadjavax.ejb.NoSuchEntityException不能在数据库中找到需要装载的数据
ejbStorejavax.ejb.NoSuchEntityException不能更新数据库中的数据
所有方法EJBException系统问题发生