J2EE 组件开发:实体EJB(上)

时间:2022-09-19 17:06:33
提纲:

======================

一、概述

  1.1 持久化

  1.2 共享访问

  1.3 主键

  1.4 关系

二、实体Bean与JDBC

三、实体EJB组件

  3.1 Bean管理的持久化

  3.2 容器管理的持久化

========================

正文:

========================
一、概述

实体Bean代表着持久性数据存储系统(通常是数据库)中的一个实体。与消息驱动的Bean、会话Bean相比,实体Bean的特点主要表现在持久化(Persistent)、共享访问、拥有主键、关系这四方面。

1.1 持久化

实体Bean的状态信息保存在数据库之类的持久性存储系统中,因此它有持久化的特点。这意味着,即使在应用或J2EE服务器进程的生存周期之外,实体Bean的状态信息仍旧有效。实体Bean的持久化分两种类型:Bean管理的持久化(Bean-Managed Persistent,BMP),或容器管理的持久化(Container-Managed Persistent,CMP)。持久化类型可以在EJB部署描述器中声明。

对于Bean管理的持久化,开发者应当自己编写访问数据库的代码。例如,ejbCreate()执行一个SQL的插入命令,开发者必须自己构造和执行SQL INSERT命令和其他相关的调用。

对于容器管理的持久化,容器自动完成必要的数据库调用。例如,当客户程序请求创建一个实体Bean,容器就生成一个SQL INSERT命令。开发者编写的代码不包含任何SQL调用。容器还能够自动同步实体Bean的状态信息与数据库中保存的数据,这些状态信息通常称为“容器管理的域”(Container-Mananged Field)。容器管理的域也在部署描述器中声明。

容器管理的持久化具有两个显著的优点:首先,容器管理持久化的实体Bean代码量较少;第二,由于Bean不包含任何数据库调用,因此它具有中立于数据库类型的特点。当然,这些优点有时会变成局限,在一些需要高度灵活性的场合,Bean管理的持久化将是首选的方案。例如,当容器不支持CMP实体Bean,或者开发者想要对数据处理方式拥有更多控制权,或者某个数据源过于复杂、难于实现高效的CMP实体Bean映射时,我们应该选用BMP实体Bean。

1.2 共享访问

实体Bean可以由多个客户程序共享。由于多个客户程序可能修改同一个数据,因此,为实体Bean提供事务(Transaction)支持就很重要。一般地,事务管理机制由EJB容器提供,开发者无需在Bean里面设置事务的界限。事务属性可以在Bean的部署描述器中指定。

1.3 主键

每一个实体Bean有一个唯一的对象标识符。例如,一个Customer实体Bean可以通过客户编号识别。实体Bean的唯一标识符也称为主键(Primary Key),它使得客户程序能够定位特定的实体Bean实例。当EJB客户程序通过实体Bean的接口创建或寻找特定的数据库记录,容器把一个主键关联到返回给客户程序的EJB实体Bean句柄。例如,如果在Customer实体Bean里面,客户的名称是主键,则对于容器来说,这个名称应该唯一地标识出数据库里面Customer表的一个记录;从客户程序的角度来看,它映射到了一个唯一的实体Bean实例。

1.4 关系

正如关系数据库中的表,实体Bean之间也可以建立关系。关系的实现方式根据持久化类型的不同而不同。对于Bean管理的持久化,关系需要开发者编写代码实现;对于容器管理的持久化,关系由容器负责实现。由于这个原因,对于容器管理持久化的实体Bean,关系通常被称为容器管理的关系(Container-Managed Relationship)。

根据实体Bean的特点,推荐使用实体Bean的场合包括:

当Bean侧重于描述业务实体而不是业务过程时。

当Bean的状态信息需要持久化时。

二、实体Bean与JDBC

连接和访问数据库是绝大多数J2EE应用的基本要求,J2EE通过JDBC提供这方面的支持。一些厂商(如Oracle)以及下一版本的J2EE规范将支持通过SQLJ访问数据库,但本文仍以JDBC为基础介绍数据库访问。J2EE平台提供对JDBC API的支持,允许我们方便地通过XML形式的部署描述器配置JDBC资源,通过JNDI连接JDBC资源。

JDBC驱动程序的配置、数据源的声明通过XML格式的J2EE模块部署描述器完成。会话Bean和实体Bean都可以在J2EE部署描述器中定义 元素,每个EJB可以配置零个或多个JDBC资源。

例如,下面的例子显示了如何在ejb-jar.xml文件中为实体EJB配置JDBC资源:

<ejb-jar>
  ...
  <enterprise-beans>
    <entity>
      ...
      <resource-ref>
        <res-ref-name>jdbc/Cloudscape</res-ref-name>
        <res-type>javax.sql.DataSource</res-type>
        <res-auth>Container</res-auth>
        <res-sharing-scope>Shareable</res-sharing-scope>
      </resource-ref>
    </entity>
  </enterprise-beans>
  ...
</ejb-jar> 


经过上述配置之后,在实体EJB之内利用数据资源就变得非常简单,只需在EJB之内查找命名后的数据源,然后获得一个javax.sql.DataSource对象的句柄。利用DataSource对象可以得到一个java.sql.Connection对象。怎样把Connection对象分配给Bean由容器决定,但一般地,容器会设立一个数据库连接池。接下来,Bean可以按照通常所做的那样,利用Connection对象构造JDBC数据库操作命令,提取结果集。

如果实体Bean的持久化由Bean管理,它必须自己管理数据库连接。虽然本文只通过实体EJB访问JDBC资源,但JDBC资源还可以从会话Bean访问。如果要在会话Bean之内访问JDBC资源,在EJB部署描述器 元素的 子元素之内,通过一个或者多个 元素进行配置。

三、实体EJB组件

实体Bean是描述数据库数据逻辑单元的Java类,实体Bean类定义的域映射到数据库模式定义的构成元素。最简单的情形下,实体Bean的域直接映射到数据库表的列,实体Bean本身就相当于一个数据库的表,实体Bean之间的关系与表之间的关系相似。视图、表的连接结果也可以看作实体Bean,此时这些数据资源的构成元素对应着实体Bean类的域。

如果实体Bean实例的域包含有效数据,则该实例对应着数据源里面的一个具体的数据单元,或者说,一个实体Bean的实例对应着表里面一个特定的记录,或者对应着表连接查询的一个结果。

3.1 Bean管理的持久化

构造BMP实体Bean的许多概念和技术同样适用于CMP实体Bean。然而,正如我们下面将看到的,BMP实体Bean要求手工编写大量的代码。在某些情形下,编写BMP实体Bean和从头开始编写基于JDBC的组件相差无几,差别仅在于JDBC逻辑的封装方式,特别地,BMP实体Bean上的标准方法暗示了将要创建和处理哪些JDBC语句。

图一显示了构造BMP实体Bean所涉及的基本体系结构。

J2EE 组件开发:实体EJB(上)


公用的、非最终的、非抽象的实体Bean,比如图一显示的MyBeanManagedEntityEJBean,必须实现javax.ejb.EntityBean接口。EntityBean接口从EnterpriseBean接口扩展得到。EntityBean接口定义了一组实体Bean必须实现的操作,这组操作是EJB容器管理Bean的生命周期所必需的。当然,Bean管理持久化的实体Bean还可以实现业务方法,比如图一显示的someMethod()和anotherMethod()方法。一般地,Bean管理持久化的实体Bean还要以域变量的形式定义一些状态信息,在这些状态信息中包含数据库记录值的缓冲版本。最后一点是,实体Bean必须有一个公用的、没有参数的构造函数,而且不应该实现finalize()方法。

javax.ejb.EntityContext接口由容器实现,它提供了一个指向实体Bean运行时上下文的句柄。EntityContext从更一般化的EJBContext接口扩展得到,并定义了一个获取远程EJB接口句柄的方法,以及一个获取Bean实例主键的方法。在一个实现EntityBean接口的类中,容器构造好Bean的实例之后,就会立即调用Bean的setEntityContext()方法。为便于以后使用,Bean应该在一个域中保存这个值。实体Bean还应该实现unsetEntityContext()方法,当实体Bean断开与容器上下文的联系时,容器将调用unsetEntityContext()方法。

当客户程序想要寻找那些与特定数据单元对应的实体Bean的实例时,容器将调用实体Bean上的ejbFindXXX(...)方法之一(也称为查找器方法)。ejbFindXXX(...)方法扮演的角色与数据库操作中SQL SELECT命令的角色相似。在执行这些方法时,如果出现了应用层错误,BMP实体Bean一般应该抛出一个FinderException异常。

所有的实体Bean都必须定义ejbFindByPrimaryKey()方法。这个方法必须以实体Bean的主键为参数,验证由主键标识的数据单元确实存在于数据库之中,然后把主键返回给容器。如果指定的数据单元不存在,BMP实体Bean应该抛出ObjectNotFoundException异常。ObjectNotFoundException异常是FinderExcetion的子类,表示不能找到指定的目标。一般地,BMP实体Bean在ejbFindByPrimaryKey()方法中构造一个SELECT命令,通过JDBC查询数据库,以此验证带有指定主键的数据确实存在于数据库之中。

除了ejbFindByPrimaryKey()方法之外,BMP实体Bean还可以定义多个附加的ejbFindXXX(...)方法,这些方法带有零个或者多个与业务有关的输入参数,根据这些参数的要求在数据库中查找符合条件的数据单元。对于每一组找到的数据单元,ejbFindXXX(...)方法必须返回与这些数据单元对应的主键的集合。在实现这些方法时,我们可以利用JDBC API执行SQL SELECT命令,然后利用结果集构造出相关主键对象的集合。如果查询返回的结果集中不包含主键,则返回值应该是一个空的集合。

实体Bean可以定义多个ejbCreate()方法,也可以没有ejbCreate()方法;这些ejbCreate()方法可以带多个参数,也可以不带参数。从功能来看,这些可选的ejbCreate()方法与数据库操作中的INSERT命令类似:在当前实体Bean类型对应的数据源中插入一个数据单元,用实体Bean获得的各种数据填写这个数据单元。实体Bean可以通过构造Bean实例期间对域变量的初始化获得这些数据,另外,也可以使用所有作为ejbCreate(...)方法参数传入的数据。容器根据客户程序的请求调用实体Bean的ejbCreate(...)方法,ejbCreate()方法返回的对象代表着新创建实体Bean的主键。

BMP实体Bean必须实现必要的数据库连接逻辑,以便ejbCreate()方法执行数据库的SQL INSERT命令。完成这一任务的基本工具是JDBC API,我们可以通过XML形式的数据源配置选项对它进行配置。如果由于某种原因不能创建实体Bean,BMP实体Bean可以抛出CreateException异常。如果不能创建实体Bean是由于已经存在具有相同主键的数据单元,则BMP实体Bean可以抛出CreateException的子类DuplicateKeyException异常。

对应于每一个ejbCreate(...)方法,实体Bean必须定义一个有着相同输入参数的ejbPostCreate()方法。当容器完成ejbCreate()方法调用,且把实体Bean的实例与客户端引用关联起来之后,接下来它就会调用ejbPostCreate()方法。因此,所有需要在ejbCreate()方法执行完毕之后进行的初始化操作,都可以在ejbPostCreate()方法之内完成。如果ejbPostCreate(...)方法执行时出现了应用层的错误,它也可以抛出CreateException异常。

当客户程序请求拆除实体Bean实例时,容器将调用ejbRemove()方法。实体Bean的ejbRemove()方法把当前关联的数据单元从数据库删除,在功能上类似于SQL的DELETE命令。BMP实体Bean应该使用EntityContext定义的getPrimaryKey()方法获得主键的句柄,然后为该数据单元构造出SQL DELETE命令。如果试图删除数据时出现了错误,ejbRemove()方法可以抛出RemoveException异常。

类似于有状态会话Bean,实体Bean也必须实现ejbPassivate()方法和ejbActive()方法。容器把实体Bean的实例返回给缓冲池之前,会调用ejbPassivate()方法。ejbActivate()方法执行的操作基本上是ejbPassivate()操作的逆向过程,容器根据客户程序对Bean实例的请求,要把以前已经钝化的Bean转入活动状态时,就会调用ejbActive()方法。

ejbPassivate()方法和ejbActive()方法用来关闭和打开非数据源的资源,而ejbLoad()方法和ejbStore()方法用来处理数据库数据。当容器要对Bean里面域的状态和保存在数据库里面的数据进行同步时,它就会调用ejbStore()方法或ejbLoad()方法。

如果容器要用Bean实例里面的数据更新对应的数据库记录,它调用ejbStore()方法。因此,从功能上看,ejbStore()方法相当于执行SQL的UPDATE命令。一般地,当ejbStore()方法被调用时,BMP实体Bean通过JDBC构造和执行SQL UPDATE命令。

如果容器要用数据库里面的最新数据更新实体Bean的状态,它调用ejbLoad()方法。也就是说,ejbLoad()操作更新的是数据单元在内存中的实体Bean实例。通常,ejbLoad()方法首先在关联的EntityContext上调用getPrimaryKey()方法,确定与该实体Bean实例关联的数据单元。然后,Bean通过JDBC查询数据单元的最新数据,并更新Bean的状态。

3.2 容器管理的持久化

如前所述,构造BMP实体Bean是一件比较繁琐的事情,与直接以JDBC为基础编写访问数据库的应用相比,编写BMP实体Bean时手工编写代码的工作量简直不相上下。不过,EJB还提供了一种简化实体Bean编写的办法,这就是容器管理持久化(CMP)的实体Bean。使用CMP实体Bean时,容器为我们实现所有JDBC数据库访问逻辑。也就是说,无论是把数据从实体Bean对象转移到关系数据库,还是从关系数据库转移到实体Bean对象,支持CMP实体Bean的容器担负起了必不可少的“对象到关系”的映射。

下面的说明主要针对开发CMP实体Bean与开发BMP实体Bean的不同之处,不再重复说明CMP和BMP相同的一些概念。

图二显示了构造CMP实体Bean所涉及的基本体系结构。公用的、非最终的、非抽象的CMP实体Bean,比如图二显示的MyContainerManagedEJBean,必须实现EntityBean接口。当然,容器管理持久化的实体Bean还可以实现业务方法,比如图二显示的someMethod()和anotherMethod()方法。另外还要注意的是,CMP实体Bean也必须有一个公用的、不带参数的构造函数,且不应该实现finalize()方法。

J2EE 组件开发:实体EJB(上)


应该特别注意的是,CMP必须把全部由容器管理的域定义成public,并确保相关的类型是可串行化的(Serializable)。由容器管理的域直接映射到与CMP实体Bean关联的数据单元的元素,例如,一个容器管理的域可以直接映射到数据库表的一个列。容器通过EJB部署描述器中的定义获知哪些域将由容器管理。

CMP实体Bean的主键可以指定为Bean的一个域,也可以先定义一个独立的包含一个或者多个公用域名字的类(这些公用域名字与CMP实体Bean的域名字对应),然后通过这个类定义主键。

要设置或取消与CMP实体Bean关联的EntityContext,我们分别使用setEntityContext()方法和unsetEntityContext()方法。正如BMP实体Bean,容器在CMP实体Bean上调用这些方法也出于同样的原因,且调用时机在Bean生命周期中的位置也相同。

CMP实体Bean不再要求开发者手工编写ejbFindXXX(...)方法。对于CMP实体Bean,容器从EJB部署描述器读取相关的配置信息,然后决定如何实现这类方法。因此,对于开发者来说,这些方法的实现过程是完全透明的。这些方法的具体实现机制,可能是由EJB支持平台在CMP实体Bean的子类中完成,也可能是通过一些捕获调用并返回结果的独立部件完成。但不管底层的实现机制如何,CMP实体Bean的开发者不必再关注如何编写代码支持这些ejbFindXXX(...)操作。

对于CMP实体Bean,ejbCreate(...)方法也是可选的,而且Bean开发者无需为了支持这些方法而编写数据库INSERT操作之类的代码。需要开发者保证的是,每一个由容器管理的域都用合适的默认值或传递给ejbCreate(...)方法的参数值正确地初始化。CMP实体Bean必需实现与ejbCreate(...)方法匹配的ejbPostCreate(...)方法。

从数据库删除与CMP实体Bean关联的数据之前,容器调用Bean的ejbRemove()方法。编写ejbRemove()方法时,开发者只需关注那些从数据库删除数据之前必须执行的操作,实际从数据库删除数据的所有代码由容器实现,且容器将在必要的时候抛出适当的EJB异常。

ejbPassivate()方法和ejbActivate()方法必须由CMP实体Bean开发者自己实现,这一点与BMP实体Bean相同。在实现ejbPassivate()方法时,开发者应该清除所有与数据源无关的资源;相应地,在实现ejbActivate()方法时,则应该重新获得这些资源。

对于大多数原来BMP实体Bean在ejbLoad()和ejbStore()方法内实现的操作,CMP实体Bean的容器也将自动处理。在BMP实体Bean中,实际的数据存储操作由ejbStore()方法实现,现在这些操作由容器负责,开发者只负责为所有容器管理的域准备好将要保存到数据库的值,比如获取那些容器管理的域的最新值。类似地,实现ejbLoad()方法时,CMP实体Bean的开发者只负责“传播”各种对Bean域值的修改。当容器完成对容器管理的域值的更新之后,容器调用ejbLoad()方法,ejbLoad()方法应该把所有的改动结果反映到其他与此相关的状态数据。

本文后面将提供一个BMP实体Bean开发的例子。这里我们先来看看CMP实体Bean开发过程中的一些要点。首先,CMP实体Bean类必须定义成public和abstract。同时,CMP实体Bean类必须实现:

  • EntityBean接口。
  • 零个或者多个ejbCreate()方法和ejbPostCreate()方法。
  • 为持久化域和关系域定义get和set访问器方法,这些方法必须定义成abstract。
  • 各种select方法,且这些方法必须定义成abstract。
  • 业务方法。


容器管理持久化的实体Bean可以拥有持久化域和关系域,这些域都是虚的(virtual),因此它们不是作为实例变量直接编写到类里面,而是在部署描述器中指定。容器自动对CMP 实体Bean的持久化域进行数据库存储和提取操作。为支持对这些域的访问,CMP实体Bean必须定义抽象的访问器方法。

访问器方法的名字以get或set开头,后面跟上首字母大写的持久化域或关系域的名字。例如,假设CMP EJB的部署描述器指定了name和salary两个持久化域,则EJB里面必须定义如下访问器方法:

public abstract String getName(); 
public abstract void setName(String name); 

public abstract double getSalary(); 
public abstract void setSalary(double salary);


又如,假设一个运动员可以属于多个组,则一个PlayerEJB实例可以关联到多个TeamEJB实例。为描述这种关系,PlayerEJB在部署描述器中定义了一个关系域teams。在PlayerEJB类中,teams关系域的访问器方法定义如下:


public abstract Collection getTeams(); 
public abstract void setTeams(Collection teams);


CMP实体Bean还可以定义select方法。select方法与查找器方法有一些相同的特点:

  • select方法查询数据库,返回查询结果。
  • 部署描述器为select方法指定EJB QL查询。
  • CMP 实体Bean类不实现select方法,而是由容器负责实现。


然而,select方法又与查找器方法有着明显的区别。例如,select方法不在任何本地接口和远程接口中导出,因此不能由客户程序调用,只能由实体Bean里面实现的方法调用。通常,select方法由业务方法调用。select方法必须按照以下规则声明:

  • 方法的名字必须以ejbSelect为前缀。
  • 方法的访问控制修饰符必须是public。
  • 方法必须定义成abstract。
  • throws子句必须包含javax.ejb.FinderException。


下面是一个声明select方法的例子:


public abstract Collection ejbSelectSports(LocalPlayer player) 
throws FinderException;


下表比较了CMP实体Bean与BMP实体Bean实现上的差异。

项目 容器管理的持久化 Bean管理的持久化
类定义 abstract 非抽象
数据库调用 由工具生成 由开发者编写
持久化状态信息 由持久化的虚域(Virtual Field)描述 作为实例变量编写
持久化域和关系域的访问器方法(get方法和set方法) 必须
findByPrimaryKey方法 由容器负责 由开发者编写
定制的查找器方法 由容器负责,但开发者必须定义EJB QL查询 由开发者编写
select方法 由容器负责
ejbCreate()方法的返回值 null 主键


另外,不论是哪种持久化类型,业务方法的编写规则都是一样的。

在《J2EE 组件开发:实体EJB(下)》中,我们将继续了解实体EJB的客户端接口(Home接口和远程接口),并通过实例了解实体EJB的开发过程和注意事项。