44.6. 容器管理持久性的Entity Bean

44.6.1. 概述

Entity Bean组件用于表示保存于持久存储中的业务实体,而这些业务实体通常是关系型数据库中的某个表。Entity Bean作为业务实体的面向对象表示,提供用于表示对象状态的域和可操作的方法,Entity Bean的域通常映射为业务实体表中的某一列,而一个Entity Bean实例通常用于表示表中的一行数据。

在Bean管理持久性的情况下,开发者必须在每个Bean中提供如何从数据库中装入数据到EJB实例和将实例状态存储到数据库中的代码,在更复杂的情况下,开发者还需要提供更多的编码,以方便地对持久存储中的业务数据进行创建、删除、同步等等操作。

通过容器对Entity Bean的持久性进行管理,开发者通过部署描述定义Entity Bean中需要容器进行管理的域,并指定域与数据库中列的映射,在Bean中定义访问这些域的抽象方法(Java Bean模型中的getter、setter方法)即可。对于被指定由容器管理持久性的域,必需的对数据库中数据的访问机制则可以完全由容器进行实现和管理,开发者不需要再编写访问、同步数据的代码,可以直接通过访问方法,即getter或setter方法,对域的状态进行访问。

由于Entity Bean都会用于表示数据库中的保存业务实体的表,而关系型数据库通常使用表间关系对数据的引用完整性进行约束。当使用容器管理持久性的Entity Bean时,可以对实体间的关系进行声明,由容器实现关系语义的面向对象的表示。

容器管理持久性给Entity Bean带来了更大的可移植特性和更大的独立性,当组件的内部实现发生更改,或其对应的数据存储结构发生更改,客户端不需更改访问Bean的代码并重新编译,同样,当Entity Bean映射的数据存储发生更改,组件也不存在需要改变的访问具体数据的代码,只需重新定义数据存储与Bean的域之间的映射。

不同于Bean管理持久性的Entity Bean,容器管理持久性给Entity Bean的开发者带来了更大的便利,和更少的需要调试的代码。 开发者通过部署描述指定容器需要管理的数据访问的域和关系,在开发时即可直接通过方法对持久数据进行访问。

容器管理持久性的Entity Bean带来的好处是,通过使用部署描述文件和简单的组件代码,即可将业务实体的逻辑表示与具体的数据存储分离,并且消除组件中复杂且易出错的数据访问的代码。

44.6.2. 容器管理持久性的Entity Bean

Entity Bean是数据库中或已有企业应用中存储的一个业务实体或一组相互紧密关联业务实体的对象表示。

容器管理持久性主要包含两个方面的特征,即对Entity Bean状态数据的持久性管理和Entity Bean间关系的管理。

44.6.2.1. CMP模型

在EJB1.1规范中的CMP模型,开发者通过部署描述将Entity Bean实例中的成员变量映射到某个数据库表,容器负责这二者之间的数据同步,但是,EJB1.1规范中的CMP模型缺乏控制业务实体间的复杂关系的机制。

保存在数据库中的业务实体以表的形式存在,表与表之间处于对等的地位,没有具体的从属关系,使用数据库表所表示的业务实体之间的逻辑关系及数据的引用完整性通常通过主键与外键来进行约束,而通过EJB1.1规范中的CMP模型进行数据的对象表示,对于单个的Entity Bean和对应的数据库表而言,可以提供表示,但是企业应用中使用数据库表所表示的业务实体之间往往存在很复杂的逻辑关系,虽然针对这一点,有经验的开发者提出了一些解决方案,但是,EJB1.1规范中的CMP模型远未完善。

在EJB2.1规范中,对CMP模型进行了比较大的改进,除了完善Entity Bean与数据库表间的数据映射,还增加了新的Entity Bean间的关系模型,以此提供数据库表从数据到逻辑关系在Entity Bean编程模型的完整映射,同时对于EJB1.1规范中难于解决的从属对象(dependent object),提供了解决办法。同时,为使用新的CMP模型对Entity Bean中的数据查询进行编写,定义了一套新的查询语言EJB QL。通过按照EJB QL规范编写的查询语句,可以对Entity Bean中的相关查询方法的语义进行定义,如EJB1.1规范中已出现的finder方法和EJB规范2.0版本中的select方法,并可在相互关联的Entity Bean的关系中进行定位,容器将使用EJB QL语句编写的查询,对关联的查询方法进行定义。

44.6.2.2. 例子说明

本节使用如下的范例对容器管理持久性的Entity Bean加以说明。通过实例介绍Apusic应用服务器中,容器管理持久性的Entity Bean在应用程序开发效率方面带来的提高和成本方面的降低。本节中将采用如下图所示的三个业务实体进行说明:

范例

通常,从上面描述的业务实体模型,可以得到如下的关系型数据库的表模型:

范例表模型

可以看出,在业务实体转换为关系型数据库中的表模型时,通常会使用主键(Primary Key)、外键(Foreign Key)或使用一个用于描述关系的辅助表描述表间的关系,以达到引用完整性的目的。

根据此范例关系模型和数据库表模型,按照如下命名规则,为其定义本章中使用的范例的数据库表名、列名和Entity Bean组件名、域名。

  • 表名按照“<实体名>_TABLE”的方式命名:BOOK_TABLE,PUB_TABLE,AUTHOR_TABLE,BOOK_AUTHOR_TABLE;

  • 组件类名按照“<实体名>EJB”的方式命名:BookEJB,AuthorEJB,PublishingHouseEJB;

  • 组件接口(本例中的EJB组件都使用本地接口)按照“<实体名>”命名,如Book,Author,PublishingHouse;

  • 组件Home接口(本例中的EJB组件都使用本地Home接口)按照“<实体名>Home”命名;

  • 组件的抽象持久性模式名称,按照“<实体名>”命名,如Book,Author,PublishingHouse;

44.6.2.3. 抽象持久性模式(Abstract persistence schema)

抽象持久性模式的概念,对于Apusic应用服务器中容器管理持久性的Entity Bean极为关键。因为,Apusic应用服务器中的持久性管理器为Entity Bean生成数据访问逻辑,主要是通过开发者在Entity Bean类的代码和部署描述文件定义的抽象持久性模式。现阶段的Apusic应用服务器中的持久性管理器除了提供对关系型数据库和Entity Bean之间的持久性管理外,将来还可根据需要实现对传统系统(如ERP等)的持久性管理。抽象持久性模式的定义,主要涉及两个方面的要素:即容器管理持久性域和容器管理关系。

44.6.2.3.1. 容器管理持久性域(cmp-field)

为提供业务实体的面向对象表示,通常需要O/R mapping方面的技术,将对象的域与数据库中的表列进行映射,在Apusic应用服务器中的容器管理持久性的Entity Bean中,开发者需要在部署描述中将组件中的一个域映射到数据库表中的一个列,并在组件代码中,按照Java Bean规范中的setter或getter方法定义这些容器管理持久性域的访问方法,以完成映射和抽象持久性模式中容器管理持久性域的声明。容器根据开发者上面的声明和定义,在运行时负责装载、保存和同步实例变量与表中的数据,开发者或客户端可通过组件代码中定义的数据访问方法访问业务实体数据。

容器管理持久性的Entity Bean中,容器在运行时为Entity Bean生成数据访问的代码,在实例变量和底层数据存储间交换数据,这些代码包含创建、删除和在底层数据存储中查找实体数据等操作的方法。

如果使用容器管理持久性的Entity Bean来表示业务实体,则开发者不能对数据访问进行编码,所有的数据访问应由容器完成。

开发者通过部署描述中的cmp-field元素来声明容器必须为组件装入和存储的的实例变量,这些变量被称为容器管理持久性域(cmp-field)。在组件代码中,这些域必须被定义为公有的(public),且不能被定义为临时的(transient)。容器负责在执行ejbCreate、ejbRemove、ejbStore或ejbLoad等方法之前在Bean的实例变量和底层数据存储间进行数据交换,同时容器需要实现在Entity Bean的Home接口中定义的finder方法。

在Apusic应用服务器2.0版本以上,对抽象持久性模式中包含的容器管理持久性域的定义,通常如下进行:

  • 在部署描述文件ejb-jar.xml中提供组件的抽象持久性模式的名称和需要容器进行持久性管理的域名称,还需要指定实体的Primary Key对应的域。即在entity元素声明中使用abstract-schema-name定义组件的抽象持久性模式名称,并使用cmp-field定义需要容器进行持久性管理的域,最后使用primkey-field定义Primary Key域,此域必须存在于使用cmp-field定义的容器管理持久性域中。需要注意的是,对于处理实体之间的关系而定义在实体中的域,需要以容器管理关系域出现,如Book实体中的publishingHouse域,将在第 44.6.2.3.2 节 “容器管理关系(cmr)与容器管理关系域(cmr-field)”中介绍。

    以本节范例实体中的Book为例,因publishingHouse和authors域用于定义与PublishingHouse、Author等实体间的关系,不属于容器管理持久性域的范围,因此,Book只有三个容器管理持久性域:id、title和price域,其中,id域表示Primary Key域,不代表实体数据,因此,未表示在实体图形中 。按照前面的规则,有如下ejb-jar.xml片断:

    ...
      <entity>
        ...		
        <abstract-schema-name>Book</abstract-schema-name>
        <cmp-field>
          <field-name>id</field-name>
        </cmp-field>
        <cmp-field>
          <field-name>title</field-name>
        </cmp-field>
        <cmp-field>
          <field-name>price</field-name>
        </cmp-field>
        <primkey-field >id</primkey-field>		
        ...
      </entity>
    ...
  • 对于每个在ejb-jar.xml文件中声明的容器管理持久性的Entity Bean,还需要在apusic-application.xml中,使用table-name元素定义组件映射的表名称,使用field-mapping元素对容器管理持久性域与数据库表中的列进行映射,并且使用cmp-resource元素声明需要使用的数据源名称,这一数据源由ejb-jar中所有的容器管理持久性的Entity Bean以及关系辅助表共同使用。以Book组件为例,有如下apusic-application.xml片断:

    <ejb>
      ...
      <entity ejb-name="Book">
        <jndi-name>Book</jndi-name>
        <cmp>
          <jdbc>
            <table-name>BOOK_TABLE</table-name>
            <field-mapping>
              <field-name>id</field-name>
              <column-name>BOOK_ID</column-name>
            </field-mapping>
            <field-mapping>
              <field-name>title</field-name>
              <column-name>TITLE</column-name>
            </field-mapping>
            <field-mapping>
              <field-name>price</field-name>
              <column-name>PRICE</column-name>
            </field-mapping>
          </jdbc>
        </cmp>
      </entity>
      ...
      <cmp-resource>
        <jndi-name>jdbc/CMPSample</jndi-name>
      </cmp-resource>
    ...
    <ejb>
    ...
  • 最后,开发者还需要在组件类中声明对容器管理持久性域的抽象访问方法 ,即Java Beans规范中的setter和getter方法,在组件被部署到应用服务器后,容器将在运行时根据前面指定的配置信息实现这些方法,因此,开发者在定义这些方法时,必须把它们声明为抽象(abstract)方法。以Book组件为例,组件类BookEJB有如下代码片断:

    ...
    public abstract class BookEJB implements EntityBean {
    
        // access methods for persistent fields
    
        public abstract void setId(java.lang.String id);
        public abstract void setTitle(java.lang.String title);
        public abstract void setPrice(double price);
        public abstract double getPrice();
        public abstract java.lang.String getTitle();
        public abstract java.lang.String getId();
        ...
    }

    在Entity Bean的开发过程中,对组件状态的访问必须通过上面定义的抽象方法进行,例如,在BookEJB中的ejbCreate方法被调时,可直接调用以上访问方法进行状态的初始化,如下:

    ...
        public String ejbCreate(
            String id,
            String title,
            double price) throws CreateException {
    
            setId(id);
            setTitle(title);
            setPrice(price);
            return null;
        }
    ...
  • 如果,业务实体的状态对客户是可读或可写的,则可以通过组件接口决定是否暴露状态域的setter和getter方法,也可根据实际情形,决定暴露哪个方法给客户端。以Book组件为例,其两个容器管理持久性域对客户而言是可读和可写的,因此,本地接口Book中关于客户访问的代码片断如下:

    ...
    public interface Book extends EJBLocalObject {
        public String getId();
    
        public void setTitle(String title);
        public String getTitle();
    
        public void setPrice(double price);
        public double getPrice();
        ...
    }

当组件部署到Apusic应用服务器,在运行时容器为组件生成所有的数据访问代码,并对状态的同步、并发访问、事务控制等进行管理,组件中其它需要访问持久存储数据的代码,则通过上面组件类中定义的setter和getter方法进行访问,客户端则通过组件接口中暴露的方法,对组件状态进行访问。

44.6.2.3.2. 容器管理关系(cmr)与容器管理关系域(cmr-field)

前面描述过,EJB1.1中,容器管理持久性的Entity Bean在处理实体间关系上的不足之处,因此,在EJB2.1规范中,容器管理持久性的Entity Bean编程模型,定义了容器管理关系模型,以此解决EJB1.1中的不足。

容器管理持久性的Entity Bean的容器管理关系,有以下三种:

  • one-to-one,每个Bean实例关联另一个Bean的单个实例;

  • one-to-many,一个Bean关联另一个Bean的多个实例;

  • many-to-many,Bean的多个实例关联另一个Bean的多个实例;

容器管理关系的方向可以是双向或单向的。在一个双向的关系中,涉及的Bean都有一个关系域与另外的Bean关联,通过关系域,可以从一个Bean的实例中访问关系另一方的Bean对象,反之亦可。在一个单向的关系中,只有一个Bean拥有关联其他Bean的关系域,只能从这个Bean的实例访问被关联的Bean对象,而不可从被关联的Bean对象访问到这个实例。

Entity Bean编程模型中定义的EJB QL查询语言用于对具有容器管理关系的Entity Bean实例之间进行定位。容器管理关系的方向决定了从一个Bean定位另一个Bean的能力。如Book作为关系中的一方,出版社PublishingHouse作为关系中的另一方,如果二者拥有双向关系,即从出版社可以知道出版了哪些书,而由书实例的状态数据亦可知道由某个出版社出版,那么,就可以通过这种双向关系,可以从出版社定位到书籍,也可以从书籍定位到出版社。

容器管理关系通过在部署描述中对需要由容器管理关系域进行声明,通过容器管理关系域来实现容器管理关系。如上面的例子,为完成PublishingHouseEJB与BookEJB间的关系,PublishingHouseEJB中需要定义关联到BookEJB的容器管理关系域为books,类型为java.util.Collection,表示关联BookEJB实例集合,通过books域可以定位到关联的BookEJB实例;在BookEJB中定义的关联到PublishingHouseEJB的容器管理关系域为publishingHouse域,类型为PublishingHouseEJB的本地接口,通过publishingHouse域,BookEJB实例可以定位到关联的PublishingHouseEJB实例。

因此,在容器管理关系中,构成关系的基础是容器管理关系域,但是,并不是所有用于在容器管理关系间进行定位的容器管理关系域,都会映射到持久存储中,以Book与PublishingHouse间的关系为例,Book中的容器管理关系域publishingHouse,被对应到BOOK_TABLE中的PUB_FID列,而PublishingHouse中的books域,则不需要保存到持久数据存储,容器将根据开发者定义的关系对books域进行求值。同时对books的类型java.util.Collection赋予与标准的Java API不尽相同的语义。

根据容器管理关系的类型,决定了关系具有基数性(cardinality),即一个cmr-field的域的get方法必须返回一个Entity Bean的本地接口或由Entity Bean的本地接口构成的集合(java.util.Collection或java.util.Set),同样,set方法的参数必须是一个Entity Bean的本地接口或由Entity Bean的本地接口构成的集合(java.util.Collection或java.util.Set)。

需要注意的是,容器管理关系定义的范围限于在同一个部署描述文件ejb-jar.xml中,即当两个Entity Bean在不同的部署描述文件ejb-jar.xml中进行声明,则不能对他们之间建立起容器管理关系,只能对在同一个部署描述文件中声明的Entity Bean之间定义容器管理关系。

在Apusic应用服务器2.0版本以上的容器管理持久性Entity Bean中,对容器管理关系的声明主要通过以下步骤进行:

  • 在ejb-jar.xml文件中,使用relationships元素中的ejb-relation元素对每个容器管理关系进行定义。在ejb-relation元素中,需要使用ejb-relationship-name元素定义标识关系的关系名称,使用ejb-relationship-role对关系中涉及的角色进行定义,每个角色涉及的角色名、多重性(one or many)、对应的Entity Bean组件、容器管理关系域,依次使用ejb-relationship-role-name、multiplicity、relationship-role-source、cmr-field元素对其进行定义。以本章中Book实体与PublishingHouse实体间的关系为例,则有如下ejb-jar.xml文件有关容器管理关系的片断:

    <ejb-jar>
      ...
      <relationships>    
      ...
        <ejb-relation>
          <ejb-relation-name>BookAndPublishingHouse</ejb-relation-name>
          <!-- One role: Book -->
          <ejb-relationship-role>
            <ejb-relationship-role-name>
              Book
            </ejb-relationship-role-name>
            <multiplicity>Many</multiplicity>
            <relationship-role-source>
              <ejb-name>Book</ejb-name>
            </relationship-role-source>
            <cmr-field>
              <cmr-field-name>publishingHouse</cmr-field-name>
            </cmr-field>
          </ejb-relationship-role>
          <!-- The other role: PublishingHouse -->
          <ejb-relationship-role>
            <ejb-relationship-role-name>
              PublishingHouse
            </ejb-relationship-role-name>
            <multiplicity>One</multiplicity>
            <relationship-role-source>
              <ejb-name>PublishingHouse</ejb-name>
            </relationship-role-source>
            <cmr-field>
              <cmr-field-name>books</cmr-field-name>
              <cmr-field-type>java.util.Collection</cmr-field-type>
            </cmr-field>
          </ejb-relationship-role>
        </ejb-relation>
        ...
      </relationships>
      ...
    </ejb-jar>
  • 在apusic-application.xml中,开发者需要使用ejb元素中包含的relationship-mapping元素对构成容器管理关系的容器管理关系域与持久存储中的数据模型进行映射。其中,使用ejb-relation-name元素定义关系的名字(需要与ejb-jar.xml文件中对应的ejb-relation-name元素的值相同)、使用table-name定义关系域所存在的表名字、使用source-role和sink-role元素定义参与关系的角色双方,完成组件模型中的容器管理关系域与数据库表中列的映射。source-role和sink-role元素被用于区分两个不同的角色,需要与在ejb-jar.xml文件中使用ejb-relationship-role元素定义的角色对应;如角色存在容器管理关系域在持久存储中的映射,则需要在source-role或sink-role元素中,使用field-mapping元素声明关系域与数据库表列的映射。如角色不存在容器管理关系域在持久存储中的映射,则可以使用空标记声明角色。

    field-mapping元素中,使用field-name元素声明本角色在ejb-jar.xml文件中定义的primkey-field域,使用column-name元素将其映射到引用此域以保存关系的列。

    以Book与PublishingHouse间的关系为例,此关系中只有Book中的关系域publishingHouse存在持久数据存储中的对应列PUB_FID,因此,有关apusic-application.xml片断如下:

    ...
    <apusic-application> 
      ...
      <module>
        ...
        <ejb>
          ...
          <relationship-mapping>
            <ejb-relation-name>
              BookAndPublishingHouse
            </ejb-relation-name>
            <table-name>BOOK_TABLE</table-name>
            <auto-create-table/>
            <source-role/>
            <sink-role>
              <field-mapping>
                <field-name>id</field-name>
                <column-name>PUB_FID</column-name>
              </field-mapping>
            </sink-role>
          </relationship-mapping>
          ...
        </ejb>
      ...
      </module>
    ...
    </apusic-application>

  • 在关系双方的组件类中,声明所使用的容器管理关系域的抽象访问方法,如关系是单向的,则只需在可定位到另一方的组件类中,对域进行声明。方法必须是公有(public)和抽象(abstract)方法。以Book与PublishingHouse间的关系为例,BookEJB和PublishingHouseEJB中的相关代码片断如下:

    //BookEJB.java
    public abstract class BookEJB implements EntityBean
    {
        ...
        public abstract void setPublishingHouse(PublishingHouse pub);
        public abstract PublishingHouse getPublishingHouse();
        ...
    }//END BookEJB.java
    
    //PublishingHouseEJB.java
    ...
    public abstract class PublishingHouseEJB implements EntityBean
    {
        ...
        public abstract void setBooks(Collection books);
        public abstract Collection getBooks();
    		  ...
    }//END PublishingHouse.java

  • 通过组件接口定义客户端对组件的访问接口,即使用组件接口决定是否允许客户端访问容器管理关系域。Book与PublishingHouse相关的组件接口代码如下:

    //Book.java
    public interface Book extends javax.ejb.EJBLocalObject
    {
        ...
        public void setPublishingHouse(PublishingHouse pub);
        public PublishingHouse getPublishingHouse();
        ...
    }//END Book.java
    
    //PublishingHouse.java
    public interface PublishingHouse extends EJBLocalObject
    {
        ...
        public void setBooks(Collection books);
        public Collection getBooks();
        ...
    }//END PublishingHouse.java

容器管理持久性域和容器管理关系域是EJB2.1中容器管理持久性的Entity Bean的基础,在提供对它们的定义之后,即可在组件中使用EJB QL定义各种finder和select方法,实现对业务数据的查询;同时,在EJB QL中,通过容器管理关系对关系中的业务实体进行定位。

依靠容器管理关系模型,Apusic应用服务器将组件业务逻辑与数据持久性分离,开发者可以更高效、快速地进行业务实体组件的开发,同时,将与持久数据存储类型有关的部分从业务逻辑中永久分离出来,由Apusic应用服务器提供高效和安全的管理。

44.6.2.3.3. 辅助值对象(dependent value object)

辅助值对象是企业应用中,较为常用一种具体(concrete)类,通常用于封装实体的全部或部分状态,用于组件内部或者通过组件接口使用辅助值对象与客户端交互。

在容器管理持久性的Entity Bean中,可使用辅助值对象表示封装实体的全部或部分状态,并作为Entity Bean中的容器管理持久性域,但是,辅助值对象不可作为容器管理关系域。

如某Customer实体包含了ContactInfo的辅助值对象,用于封装联系人姓名(contact_name)、电话(contact_phone)、传真(contact_fax)和电子邮件地址(contact_email)信息。

要将ContactInfo用于容器管理持久性的CustomerEJB,则需要将ContactInfo声明为一个容器管理持久性的域,持久性管理器将对此域及其映射在持久存储中的数据进行管理。当使用getter访问方法访问此域时,容器将返回一个辅助值对象的实例拷贝,当使用setter方法方法进行赋值时,容器将使用参数实例的拷贝进行赋值。

如果Bean使用自动建表功能,则不需要对ContactInfo与持久存储进行映射;当需要将辅助值对象映射到现有的表模型,则需要以下步骤:

  • 在组件类中声明此辅助值对象的抽象访问方法,并在ejb-jar.xml文件中,将此域声明为一个容器管理持久性的域。

    CustomerEJB.java中的相关代码片断如下:

    ...
    public abstract class CustomerEJB implements EntityBean
    {
        ...
        public abstract ContactInfo getInfo();
        public abstract void setInfo(ContactInfo info);
        ...
    }

  • 在apusic-application.xml中,映射辅助值对象的域与表模型中的列,域声明使用<容器管理持久性域的名称>.<辅助值对象的域>,如本例中,ContactInfo在CustomerEJB中以info为域名,而ContactInfo包含name、phone、fax和email域,因此,apusic-application.xml有如下配置片断:

    ...      
    <entity ejb-name="CustomerEJB">
        <jndi-name>CustomerEJB</jndi-name>
        <cmp>
        <jdbc>
          ...
          <field-mapping>
            <field-name>info.name</field-name>
            <column-name>customer_name</column-name>
          </field-mapping>
          <field-mapping>
            <field-name>info.phone</field-name>
            <column-name>phone</column-name>
          </field-mapping>
          <field-mapping>
            <field-name>info.fax</field-name>
            <column-name>fax</column-name>
          </field-mapping>
          <field-mapping>
            <field-name>info.email</field-name>
            <column-name>email</column-name>
          </field-mapping>
          ...
        </jdbc>
      </cmp>
    </entity>
    ...

辅助值对象的类必须实现java.io.Serializable接口。如果辅助值对象中的域是另一个辅助值对象类型,则前面的映射方式是可以递归的,如Customer实体中,contact_name被替换为contact_firstname和contact_lastname,在辅助值对象ContactName包含两个域firstName和lastName,以实现对contact_firstname和contact_lastname的映射,ContactName作为辅助值对象ContactInfo中的一个name域,则上面的apusic-application.xml文件中可进行如下对应:

...      
<entity ejb-name="CustomerEJB">
    <jndi-name>CustomerEJB</jndi-name>
    <cmp>
    <jdbc>
      ...
      <field-mapping>
        <field-name>info.name.firstName</field-name>
        <column-name>contact_firstname</column-name>
      </field-mapping>
      <field-mapping>
        <field-name>info.name.lastName</field-name>
        <column-name>contact_lastname</column-name>
      </field-mapping>
      <field-mapping>
        <field-name>info.phone</field-name>
        <column-name>phone</column-name>
      </field-mapping>
      <field-mapping>
        <field-name>info.fax</field-name>
        <column-name>fax</column-name>
      </field-mapping>
      <field-mapping>
        <field-name>info.email</field-name>
        <column-name>email</column-name>
      </field-mapping>
      ...
    </jdbc>
  </cmp>
</entity>

因此,对于嵌套的辅助值对象的声明和映射,可使用“.”进行定位。在EJB QL中,也可在路径表达式中对辅助值对象进行定位。

44.6.2.3.4. 自动建表与自动生成主键

以上部分描述了通过业务实体模型建立关系型数据库表模型,然后使用容器管理持久性的Entity Bean提供业务实体的面向对象表示。这方面的能力在使用传统数据库数据和传统应用数据方面具有重要的意义。

另一方面,通过Apusic应用服务器的持久性管理器,可以从业务实体模型建立起面向对象的实体组件,由容器在关系型数据库中自动创建对应的表模型和关系模型,对于企业应用的快速开发和应用开发过程中的开发、测试等过程,能起到提高开发效率和降低开发成本的作用,企业应用中需要维护的内容降到了更低的程度。

因为不同的关系型数据库提供的功能不同,因此,以往依赖于数据库的某个功能的企业应用,如使用数据库提供的自动生成主键值的功能,在一定程度上限制了企业应用的可移植特性。

Apusic应用服务器提供了一系列的功能,使应用逻辑不依赖于特定的数据库系统,并且,也不影响在某种数据库系统上的应用的性能优化。

下面对Apusic应用服务器提供的容器管理持久性Entity Bean的自动建表和自动生成主键的功能进行介绍:

44.6.2.3.4.1. 自动建表

以Book业务实体为例,前面提供了映射容器管理持久型域与数据库表列的范例部署描述,即如下的apusic-application.xml文件:

...
<entity ejb-name="BookEJB">
    <jndi-name>BookEJB</jndi-name>
    <cmp>
      <jdbc>
        <table-name>BOOK_TABLE</table-name>
        <field-mapping>
          <field-name>id</field-name>
          <column-name>BOOK_ID</column-name>
        </field-mapping>
        <field-mapping>
          <field-name>title</field-name>
          <column-name>TITLE</column-name>
        </field-mapping>
        <field-mapping>
          <field-name>price</field-name>
          <column-name>PRICE</column-name>
        </field-mapping>
      </jdbc>
    </cmp>
</entity>
...

实现自动建表的功能只需在上述部署描述文件中加入auto-create-table元素,同时,不再需要对容器管理持久性域与数据库表列的映射声明,如下所示:

...
<entity ejb-name="BookEJB">
  <jndi-name>BookEJB</jndi-name>
  <cmp>
    <jdbc>
      <datasource-name>jdbc/mssql</datasource-name>
      <table-name>BOOK_TABLE</table-name>
      <auto-create-table/>
    </jdbc>
  </cmp>
</entity>
...

持久性管理器将在启动应用时,通过开发者指定的与数据库的连接和表名,在数据库中生成相应的表结构。

另外,对容器管理关系的自动创建,也可使用自动建表的功能。这一点,对于many-to-many的关系,可被广泛利用,以减轻对关联表的管理。如本章使用的例子中的Author与Book之间的many-to-many关系,在不使用自动建立关系表的功能时,需要作如下设置:

...
<apusic-application> 
  ...
  <module>
    ...
    <ejb>
      ...
      <relationship-mapping>
        <ejb-relation-name>BookAndAuthor</ejb-relation-name>
        <table-name>BOOK_AUTHOR_TABLE</table-name>
        <source-role>
          <field-mapping>
            <field-name>id</field-name>
            <column-name>BOOK_FID</column-name>
          </field-mapping>
        </source-role>
        <sink-role>
          <field-mapping>
            <field-name>id</field-name>
            <column-name>AUTHOR_FID</column-name>
          </field-mapping>
        </sink-role>
      </relationship-mapping>
      ...
    </ejb>
  ...
  </module>
...
</apusic-application>

在使用自动建表时,可用如下设置:

...
<apusic-application> 
  ...
  <module>
    ...
    <ejb>
      ...
      <relationship-mapping>
        <ejb-relation-name>BookAndAuthor</ejb-relation-name>
        <table-name>BOOK_AUTHOR_TABLE</table-name>
        <auto-create-table/>
     </relationship-mapping>
      ...
    </ejb>
  ...
  </module>
...
</apusic-application>
44.6.2.3.4.2. 自动生成主键

企业应用中时常会使用不代表实体内部状态的数据来表示实体的主键。在基于J2EE平台的企业应用开发中,表现为应用的装配者或部署者在将应用部署到实际的企业应用平台时,使用一些特定的数据库提供的功能,自动为业务数据生成主键数据,这些数据可能是任何类型,如整型值、字符串、UUID等。

因此,开发者在开发容器管理持久性的Entity Bean时,如果Entity Bean的主键将在部署时进行指定,则开发者可以使用java.lang.Object来指定未知的主键数据类型,持久性管理器将自动为Entity Bean实例和持久存储数据生成主键。

在Apusic应用服务器中,可以按照如下方法使用自动生成主键的功能:

  • 如果声明一个Entity的主关键字类型为java.lang.Object时,持久性管理器会自动为Entity生成一个主关键字,这时不需要在apusic-application.xml中声明auto-generate-key标记。在home接口中的findByPrimaryKey方法中,使用java.lang.Object作为参数类型;在组件类中的ejbCreate方法,必须声明返回java.lang.Object类型;

  • 不可声明一个容器管理持久性的域来映射此主键,只需在ejb-jar.xml文件中使用prim-key-class元素声明Primary Key类型为java.lang.Object,且不要在ejb-jar.xml文件中使用primkey-field元素声明主键域;

  • 可调用组件接口中的getPrimaryKey方法,得到主键的值。

另外,Apusic应用服务器还提供另一种更为方便的自动生成主键的功能,即使用整型类型的主键值。即声明一个Entity的主关键字类型为java.lang.Integer,并且在apusic-application.xml中声明了auto-generate-key标记,则持久性管理器也会在创建EJB实例时自动生成一个主关键字。使用这种方法,可以简化开发者与部署者之间的交流,而且在Entity Bean内部和Entity Bean之间,简化需要对主键进行的访问操作。

当使用Apusic应用服务器中的持久性管理器自动生成实体的主键数据的功能时,可以指定主键类型为java.lang.Integer类型,主键值缺省状态下以20为单位递增,即0、20、40...。

配置容器自动生成整型类型的主键值的功能比较简单,需要在apusic-application.xml中的jdbc元素中,声明auto-generate-key空标记,

并注意如下规则:

  • 在home接口中的findByPrimaryKey方法中,使用java.lang.Integer作为参数类型;在组件类中的ejbCreate方法,必须声明返回java.lang.Integer类型;

  • 不可声明一个容器管理持久性的域来映射此主键,只需在ejb-jar.xml文件中使用prim-key-class元素声明Primary Key类型为java.lang.Integer,且不要在ejb-jar.xml文件中使用primkey-field元素声明主键域;

  • 可调用组件接口中的getPrimaryKey方法,得到主键的值,主键值可使用java.lang.Integer进行造型。

44.6.2.3.5. 装载单元

对于企业应用中的实体,并非每次对实体的操作都会涉及实体的所有状态,一般来说,某些常用的操作决定了实体内部状态被使用的频度,例如,雇员纪录中包括住址、工作经历、技能、历史工作纪录、姓名、部门、职位等相关内容,当应用中对雇员纪录的查询大多数为对姓名、部门、职位内容的查询时,每次操作都读入所有的记录数据是不必要的。因此,对于实体状态的装载,需要更为细致的控制,即当处理实例状态的装载请求时,所有状态的集合可划分为多个子集合,只有当操作涉及某个子集合时,才对此子集合进行装载。

Apusic应用服务器中,对于容器管理持久性的Entity Bean,开发者可以通过将状态划分为不同的装载单元,以实现上面描述的这种装载机制。

装载单元涉及组件的容器管理持久性域和容器管理关系域,开发者可以在apusic-application.xml中使用load-unit元素,对装载单元进行指定。如下:

      
...
<entity ejb-name="CustomerEJB">
  ...
  <cmp>
    <jdbc>
      ...
      <load-unit>
        <unit-name>customer-usaual-info<unit-name>
        <cmp-field>info.name</cmp-field>
        <cmp-field>info.email</cmp-field>
        <cmp-field>info.phone</cmp-field>      
      <load-unit>
    </jdbc>
  </cmp>
</entity>
...

Apusic应用服务器使用了惰性装载(Lazy loading),充分提高了企业应用的效率,当对组件使用划分明确的装载单元后,可以更大幅度的提高企业应用的响应能力。

44.6.2.3.6. 关系的赋值语义

关系多重性(multiplicity)的引用完整语义决定了容器管理关系的赋值操作具有特殊的语义。

例如,一个表示订单的Bean(OrderEJB)与表示订单中的采购项的Bean(LineItemEJB),从OrderEJB到LineItemEJB,这两个EJB之间被定义具有one-to-many的关系,即一个具体的订单,关联多个采购项,OrderEJB中包含一个setLineItems的方法,用于赋值给OrderEJB关联的所有LineItemEJB实例。

当o-1是一个订单实例,关联的采购项实例集合为ls1,o-2是另一个订单实例,o-2关联的采购项集合为空,当客户调用o-2的setLineItems方法,将ls1集合赋值给o-2关联的LineItemEJB实例集合时,在同一个事务上下文中,o-2关联的LineItemEJB的实例集合将被从空更改为ls1,而o-1关联的LineItemEJB实例集合为空。看起来好像ls1从o-1被移动到了o-2。

在one-to-many和many-to-many的关系中,java.util.Collection API或Entity Bean实例的setter方法,可以被用来操作一个集合类型的cmr-field域的内容。

下面将分别描述使用java.util.Collection API和使用setter这两种方法。

44.6.2.3.6.1. 使用java.util.Collection API更新关系

在容器管理持久性的Entity Bean中,具备容器管理关系的组件实例,java.util.Collection接口被用于表示具有集合值的容器管理关系域(cmr-field),如上面的订单与采购项的例子,订单拥有一个具有集合值的容器管理关系域(cmr-field)lineItems,而这些采购项集合通过java.util.Collection接口表示,使用java.util.Collection接口方法拥有的标准语义可以操纵这个集合,如size方法返回集合中元素的数目、iterator返回集合的迭代器等等,但由于one-to-many这种关系具有的引用完整性,使得这种情况下的java.util.Collection接口中的add和addAll方法具有与java.util.Collection接口方法拥有的标准语义不同的特殊语义。

  • 如果add方法的参数是在容器管理关系中定义的具有集合值类型的容器管理关系集合中的元素,如上例中的ls1集合中的某个LineItemEJB实例,则在同一事务上下文中,此元素将被从原有的关系中移出,添加到现有的关系中,如上例,假设l-1是ls1中的一个元素,当对表示o-2关联的的LineItemEJB的实例集合ls2调用add方法,其参数是l-1时,在同一事务上下文中,l-1将被从ls1中清除,并被添加到ls2中;

  • 对于addAll方法,当在one-to-many的容器管理关系中,具有与add方法相同的语义,对addAll参数中的每个元素使用add方法的语义。

由于在one-to-many关系中,java.util.Collection API所具有的特别的语义,因此,对集合的迭代器(iterator)进行操作,有如下限制:当由集合产生的迭代器在运行中,对集合元素的进行的添加或删除的操作没有使用java.util.Iterator API的remove方法时,容器将在对迭代器执行下一操作时抛出java.lang.IllegalStateException异常。下面的范例说明处理这一情况的正确方法:

Collection ls1 = o1.getLineItems();
Collection ls2 = o2.getLineItems();
Iterator i = o1.iterator();
LineItem lineItem;

// a wrong way to transfer the line-item
while (i.hasNext()) {
    lineItem = (LineItem)i.next();
    ls2.add(salesrep); // removes line-items from ls1
}

// this is a correct and safe way to transfer the line-items
while (i.hasNext()) {
    lineItem = (lineItem)i.next();
    i.remove();
    ls2.add(salesrep);
}
44.6.2.3.6.2. 使用setter方法更新关系

对于容器管理关系域的setter方法,同样具有由关系的多重性带来的引用完整语义。

在one-to-many的关系中,某一实例中容器管理关系域的集合对象被赋值到拥有同种类性的容器管理关系的另一个实例时,集合中的对象即被移动,后一实例的集合域中的对象被前一实例集合域中的对象取代,但集合不变,只是前一实例中的集合对象元素被清空。

44.6.2.4. Primary Key

Entity Bean实例通常作为关系型数据库表中的一行数据,具有一个标识唯一性的Primary Key,通过Primary Key,开发者可以在部署描述对实例与其他类型的Entity Bean实例间的容器管理关系进行定义。

对于容器管理持久性的Entity Bean实例,容器在Primary Key的基础上维护一个Entity Bean的运行时的对象标识。类似于关系型数据库中的复合键(compound key),Primary Key可以由实例的一个或多个容器管理持久性的域构成。开发者通过部署描述中的prim-key-class元素指定Primary Key数据的类型,通过primkey-field元素指定Primary Key被映射到某个容器管理持久性的域。

容器必须可以操作实例的Primary Key的类型,因此具有容器管理持久性的Entity Bean的Primary Key的类型必须遵循如下规则:

  • 必须是合法的RMI类型;

  • 必须提供hashCode方法和equals方法的正确实现,以简化容器对Primary Key的管理;

有两种方法指定一个Primay Key的类(class):

  • 映射到Entity Bean类的一个域的类;

    开发者可以使用部署描述中的primkey-field元素指定Entity Bean类中包含Primary Key的容器管理域,此域的类型必须是Primary Key的类型。

  • 映射到Entity Bean类多个域的类;

    Primary Key类必须是公有的(public),并且必须拥有一个无参数的共有构造方法(constructor)。

    Primary Key类中的所有域必须是公有的(public)。

    Primary Key类中的所有域的名字必须是所有容器管理域的名字集合的子集

44.6.2.5. 实例的清除

对于Entity对象,实例的清除也涉及引用完整性方面的因素,可以通过以下两种方式对实例进行清除。

44.6.2.5.1. remove方法

开发者可以通过调用Entity对象的remove方法对Entity对象进行清除。在remove方法被调用后,容器将调用组件的ejbRemove方法,参见 第 44.4.5 节 “生存周期” 。容器将把Entity对象从它参与的所有关系中移除,并且清除其对应的持久数据表示。

  • 当Entity对象被从关系中移除,关系所提供的可访问到此对象的方法将反映出对象被移除的结果。原通过one-to-one关系或many-to-one关系可访问到此对象的方法,现会返回null值;而通过many-to-may关系可访问到此对象的方法将返回一个不包含此对象的集合对象。

  • 对于已被移除的Entity对象的访问,如果是通过远程客户进行调用,容器将抛出java.rmi.NoSuchObjectException异常;如是通过本地客户进行调用,容器将抛出javax.ejb.NoSuchObjectLocalException异常。当把一个已被移除的Entity对象赋值给一个容器管理关系的域(无论是通过域的set方法或通过java.util.Collection API),容器将抛出java.lang.IllegalArgument异常。

另外,对于被移除对象参与的容器管理关系中关联的所有其他对象,如果其cascade-delete元素被指定,则移除操作将被级联(cascade)到这些对象。

对象的移除可能牵涉到多于一个的容器管理关系。如下例中,当Order对象关联的ShippingAddress对象被移除,则访问BillingAddress对象的方法将返回null。

public void changeAddress()
    Address a = createAddress();
    setShippingAddress(a);
    setBillingAddress(a);
    //both relationships now reference the same entity object
    getShippingAddress().remove();

    if (getBillingAddress() == null) //it must be
        ...
    else ...
        // this is impossible....

通常,对象的移除,即通过调用对象的remove方法进行的操作,只会导致此对象被移除。不会使移除操作级联(cascade)到其他对象。如要级联地自动移除其它对象,则需要使用级联(cascade)移除机制。

44.6.2.5.2. 级联(cascade)移除

在容器管理关系的部署描述中,cascade-delete元素被用于指定某些Entity对象的生命周期依赖于其它Entity对象的生存周期。换句话说,在某个容器管理关系中,当通过设置其cascade-delete元素,对参与关系的某一方设置其依赖的另一方时,如被依赖一方的对象实例被移除,容器将会把通过关系关联到的另一方的对象实例也移除。

cascade-delete元素包含在ejb-relationship-role元素中。ejb-relationship-role则被包含在ejb-relation元素中。在同一个ejb-relation元素中,只有当一个ejb-relationship-role元素的multiplicity被指定成为one时,才能对另一个ejb-relationship-role元素指定cascade-delete元素。即当容器管理关系是one-to-one的情况下,可以对关系的双方使用级联移除,当容器管理关系是one-to-many的情况下,只可对重复性为many的一方使用级联移除。

当容器管理关系中,存在指定了cascade-delete元素的一方,假定被称为B方,另一方被称为A方,A1为某个A方的对象实例,A1通过容器管理关系关联的B方的对象或对象集合为B1,则当对A方的对象A1调用remove方法时,如B1是单个的对象,则B1也被容器通过类似调用remove方法的方式移除,如B1是对象集合,则对集合中的每个对象应用上一方法移除。

使用cascade-delete元素,只会导致关系中被指定的对象的移除。

如Book与PublishingHouse之间的关系,在ejb-jar.xml中,则可如下配置:

        <ejb-relation>
            <ejb-relation-name>
                BookAndPublishingHouse
            </ejb-relation-name>
            <ejb-relationship-role>
                <ejb-relationship-role-name>
                    Book
                </ejb-relationship-role-name>
                <multiplicity>Many</multiplicity>
                <cascade-delete/>
                <relationship-role-source>
                    <ejb-name>Book</ejb-name>
                </relationship-role-source>
                <cmr-field>
                    <cmr-field-name>publishingHouse</cmr-field-name>
                </cmr-field>
            </ejb-relationship-role>
            <ejb-relationship-role>
                <ejb-relationship-role-name>
                    PublishingHouse
                </ejb-relationship-role-name>
                <multiplicity>One</multiplicity>
                <relationship-role-source>
                    <ejb-name>PublishingHouse</ejb-name>
                </relationship-role-source>
                <cmr-field>
                    <cmr-field-name>books</cmr-field-name>
                    <cmr-field-type>
java.util.Collection
                    </cmr-field-type>
                </cmr-field>
            </ejb-relationship-role>
        </ejb-relation>

44.6.2.6. Finder方法

开发者可以在Entity Bean的Home接口中定义零到多个finder方法,每个方法提供一种在同一Home接口中查找Entity对象和Entity对象集合的方式。finder方法必须以“find”开头,如findLargeAccounts(...)。finder方法的参数用在查询的实现中,用于finder方法定位需要查找的Entity对象。

除findByPrimaryKey方法之外,每个finder方法必须在部署描述中定义query元素,另外,如果Finder方法符合容器自动生成查询QL语法定义此方法需要执行的查询。有关具体的EJB QL语法,参见第 44.7 节 “EJB QL”

开发者在Entity Bean的本地Home接口和远程Home接口中,使用同样的名字和参数类型定义finder方法的情况下,在query元素中指定的查询语句同时定义这两个方法的语义。

44.6.2.6.1. 单个对象的查找

某些finder方法,被设计为返回一个Entity对象。对于这一类finder方法,如果是在Entity Bean的远程Home接口中定义,则方法的返回值类型必须是Entity Bean的远程接口;如果是在Entity Bean的本地Home接口中定义,则方法的返回值类型必须是Entity Bean的本地接口。

以下代码是远程Home接口中定义一个返回单个Entity对象的finder方法范例:

// Entity’s home interface
public interface BookHome extends javax.ejb.EJBHome {
    ...
    public Book findByPrimaryKey(String bookId)
        throws FinderException;
    ...
}
[注意]注意

本地Home接口中定义的finder方法不可抛出java.rmi.RemoteException异常。

当使用除findByPrimaryKey之外的返回单个Entity对象的finder方法时,必须保证finder方法确实返回的是单个Entity对象。因为如果在部署描述中为finder方法指定的EJB QL语句中包含了对Entity Bean的Primary Key中某个域的等式判断(equality test),这种不返回单个Entity对象的情况就有可能发生。

44.6.2.6.2. 多个对象的查找

某些finder方法可能会被指定查找多个Entity对象,对于这一类finder方法,如果是在Entity Bean的远程Home接口中定义,则方法的返回值类型必须是由Entity Bean的远程接口构成的集合;如果是在Entity Bean的本地Home接口中定义,则方法的返回值类型必须是Entity Bean的本地接口构成的集合。

对于容器管理持久性的Entity Bean,开发者使用java.util.Collection接口定义返回值的集合类型。

当在finder方法的查询语句中的SELECT子句中,未使用DISTINCT进行限制,则返回的集合类型中可能会包含重复的元素。

当使用在远程Home接口中定义的finder方法时,将集合中的元素定位到Entity Bean的远程接口时,必须使用PortableRemoteObject.narrow(...)方法。

以下代码是远程Home接口中定义一个返回多个Entity对象的finder方法范例:

// Entity’s home interface
public interface BookHome extends javax.ejb.EJBHome {
    ...
    public Collection findByTitle(String title)
        throws FinderException;
    ...
}
[注意]注意

本地Home接口中定义的finder方法不可抛出java.rmi.RemoteException异常。

44.6.2.6.3. 容器自动生成查询

Apusic应用服务器还提供了自动生成缺省查询的功能,以提高应用程序的开发效率。在以下几种情况下,容器将自动为finder方法生成缺省的查询语句:

  • 如在Home接口中声明了findAll方法, 容器将自动生成查询所有行的语句;

  • 如在Home接口中定义的finder方法名为findBy<域名>,其中,域名是一个容器管理持久性域的名字,且第一个字母大写并且参数类型与容器管理持久性域类型相同,容器自动生成按此容器管理持久性域进行查找的查询语句。

44.6.2.7. Select方法

select方法是Entity对象中用于查询的方法。不同于Finder方法,select方法不能在Entity Bean中的Home接口中定义,只是开发者定义在Entity Bean组件类中的抽象方法,并且不通过Home接口或组件接口暴露给客户端。

与finder方法相同的是,select方法也使用EJB QL查询语句定义语义,但是,select方法可以返回相当于容器管理持久性域(cmp-field)或容器管理关系域(cmr-field)的值。

每个select方法必须在部署描述中定义query元素。通过此元素,开发者在部署描述中使用EJB QL语法定义此方法需要执行的查询。有关具体的EJB QL语法,参见第 44.7 节 “EJB QL”

一般,select方法返回的Entity对象都是EJBLocalObject对象。如果select方法需要返回EJBObject对象,必须在select方法的部署描述元素query中设置result-type-mapping的值为Remote。

对select方法的调用并不基于被调用实例本身的对象标识,但是,开发者可以使用Entity对象的Primary Key作为select方法的参数,以定义特定Entity Bean实例查询的逻辑范围。

下表是select方法与finder方法语义的对比:

 finder方法select方法
方法名以“find”开头以“ejbSelect”开头
可见性客户端可见Entity Bean类内部
实例实例池状态的任意实例当前实例,可以是实例池状态,也可是就绪状态
返回值Entity Bean中的本地接口类型或远程接口类型Entity Bean中的本地接口类型、远程接口类型或容器管理持久性域(cmp-field)类型
44.6.2.7.1. 返回单个对象的select方法

某些select方法被指定只能返回一个值,当开发者定义这一类方法,必须保证方法只能返回一个值或者一个对象。如果select方法对应的查询语句对指定的类型返回了多个值,容器将抛出javax.ejb.FinderException异常。

select方法通常被定义为返回多个对象。

44.6.2.7.2. 返回多个对象的select方法

某些select方法被指定能返回多个值,对于这一类方法,返回值类型通常是对象集合。select方法返回的值对象集合需要使用java.util.Collection接口或java.util.Set接口作为返回值类型。集合中的元素类型取决于相应的EJB QL语句的SELECT子句。如果使用java.util.Collection作为返回值类型,并且SELECT子句不包含DISTINCT作为限制,则返回的java.util.Collection集合中可能包含重复的元素;如果使用java.util.Set作为返回值类型,并且SELECT子句不包含DISTINCT作为限制,则容器将把查询语句替换为SELECT DISTINCT。

下面是BookEJB中返回多个对象的select方法的范例:

// Book implementation class
public abstract class BookEJB implements javax.ejb.EntityBean
{
    ...
    public abstract java.util.Collection
        ejbSelectAllOrderedAuthors()
        throws FinderException;
        // internal finder method to find all authors ordered
    ...

44.6.2.8. 实例的生存周期与开发中的约定

第 44.4 节 “Entity Bean”中,讲述了Entity Bean共有的组件模型和组件接口,同时,从客户端的角度描述了Entity Bean的生存周期,但是,容器管理持久性的Entity Bean和Bean管理持久性的Entity Bean在组件类和组件接口的开发上,存在很多不同,因此,本节内容包含容器管理持久性Entity Bean的详细生存周期,以及开发容器管理持久性Entity Bean时,需要注意的一些约定和规则,关于Bean管理持久性的Entity Bean的开发,参见 第 44.5 节 “Bean管理持久性的Entity Bean”

44.6.2.8.1. 实例的生存周期

实例的生存周期

容器管理持久性的Entity Bean实例有如下几个状态:

  • 不存在状态 ;

  • 实例池状态。处于实例池状态中的实例没有特别的对象标识;

  • 就绪状态。处于就绪状态的实例已被分配对象标识。

容器管理持久性的Entity Bean实例的生存周期如下:

  • 实例的生存周期开始于容器创建新的Entity实例,然后容器调用实例的setEntityContext方法,将容器保存的伴随实例的EntityContext对象传递给实例。EntityContext对象使实例拥有访问容器提供的服务和取得客户端调用信息的能力;

  • 实例进入实例池中,成为可用实例。每个Entity Bean拥有其自身的实例池。实例池中的所有实例无对象标识,且都是相等的,任何实例都可通过分配对象标识进入就绪状态。对于实例池中的实例,容器可以调用实例的ejbFind方法和ejbHome方法,这两种方法的对应客户端视图在组件的Home接口中定义,执行这些方法不会导致实例改变其状态,同时可以在ejbHome方法中调用实例的ejbSelect方法。

  • 当客户端对Entity对象进行调用,容器选择某实例以服务此调用时,实例从实例池状态转移到就绪状态。这种转换由两种可能的方式:一是通过ejbCreate方法和ejbPostCreate方法,即当客户端调用了Home接口中定义的create方法,容器在创建Entity对象的过程中,将实例分配给Entity对象;另一种方式是当对已存在的Entity对象的调用发生,当没有合适的处于就绪状态的实例可分配给客户端请求时,容器通过调用ejbActivate方法,使实例由实例池的状态转移到就绪状态;

  • 处于就绪状态中的实例拥有指定的对象标识,当实例状态发生变化或与底层数据存储中的数据不一致,容器将会决定何时调用ejbLoad方法和ejbStore方法,以对实例状态和业务数据进行同步。当实例处于就绪状态,ejbSelect方法可以由业务方法或ejbStore或ejbLoad方法所调用;

  • 容器可以在事务边界中选择钝化一个Entity Bean实例。容器首先调用ejbStore方法保证实例状态与业务数据的同步,之后调用ejbPassivate方法,使实例转移到实例池的状态;

  • 最终,容器将实例转移到实例池状态。有三种方式完成转移,一种是通过ejbPassivate方法,通过ejbRemove方法,还有就是因ejbCreate、ejbPostCreate和ejbRemove等方法的事务回滚发生的转移(上图中未标注)。容器在需要解除实例的对象标识但不移除对象的情况下,调用ejbPassivate。当组件接口中或Home接口中的remove方法被调用后,容器调用ejbRemove方法移除Entity对象。当ejbCreate方法、ejbPostCreate方法和ejbRemove方法被调用,但发生了事务回滚,容器将把实例转移到实例池状态。

  • 当实例返回实例池中,不再拥有对象标识。容器可以把实例分配给任何同一Home接口的Entity对象;

  • 容器通过调用实例的unsetEntityContext方法清除实例。

44.6.2.8.2. 开发中的约定

容器管理持久性Entity Bean的开发者除必须提供EJB组件具有的Home接口、组件接口之外,提供的组件类必须是实现(extends)javax.ejb.EntityBean接口的抽象(abstract)类,而且必须按照如下约定编写组件类:

  • 提供一个无参数的公有构建器(public constructor) ;

  • 实现setEntityContext方法。

    容器通过此方法将一个EntityContext接口的引用传递给实例。如果实例希望在其生存周期内使用此引用,一般需要定义一个变量以保存此引用。在此方法中,实例的对象标识不可用,在此方法内部不可访问实例的持久状态和关系。

    通过此方法,开发者可以对实例生存周期内将要使用的资源进行分配,此类资源不可被指定到特定的Entity对象标识,因为在实例的生存周期内部,实例可能会服务于多个Entity对象标识。

  • 实现unsetEntityContext方法。

    在结束实例的生存周期之前,容器将会调用此方法。在此方法中,实例的对象标识不可用,在此方法内部不可访问实例的持久状态和关系。通过此方法,开发者可以对实例生存周期内占用的资源进行释放,此类资源通常在setEntityContext内分配。

  • 实现ejbCreate方法。

    Entity Bean的组件类中可以定义零个到多个ejbCreate方法。其方法签名必须与组件的Home接口中定义的create方法一一对应。在客户端调用create方法时,容器将调用对应的ejbCreate方法。

    开发者通过getter和setter方法,使用ejbCreate方法的输入参数,对实例进行初始化,当ejbCreate方法返回时,在底层数据存储中创建对象的持久数据表示。在此方法中,对容器管理域的getter方法的调用将返回Java语言中的缺省值(如integer型的域返回0,指针类型返回null),除了集合类型的容器管理关系域,对它的getter方法的调用将返回一个空集合。在此方法中,开发者不能试图去修改容器管理关系域,而应该在ejbPostCreate方法中进行此操作。

    ejbCreate方法创建的Entity对象必须拥有一个非重复的Primary Key,即不同于所有已经存在的,同一Home接口的Entity对象已经拥有的Primary Key。

    [注意]注意

    开发者应该在ejbCreate方法中编码返回null值,如下:

        ...
        public String ejbCreate(
            String id,
            String title,
            double price) throws CreateException {
    
            setId(id);
            setTitle(title);
            setPrice(price);
            return null;
        }
        ...

    ejbCreate方法的事务上下文由对应的create方法指定的事务属性决定。容器在开发者实现的ejbCreate方法完成后,在同一事务上下文中完成数据的插入操作。

  • 实现ejbPostCreate方法。

    每个ejbCreate方法都有一个对应的ejbPostCreate方法,这两个对应方法拥有同样的参数数目和类型,但ejbPostCreate方法的返回值类型为void,容器在调用ejbCreate方法后,使用同样参数调用ejbPostCreate方法。容器可以调用随同实例的EntityContext对象引用的getPrimaryKey方法,得到实例的Primary Key。

    在ejbPostCreate方法中,对象标识是可用的,例如,可以在方法中取得对象的组件接口并作为参数传递给另一个EJB。

    ejbPostCreate方法与ejbCreate方法在同一事务上下文中执行。

  • 实现ejbActivate方法。

    当容器从实例池中取得实例并分配给实例一个指定对象标识时,容器将调用此方法,开发者可以在此方法中为实例将在就绪状态中使用的资源进行分配。在此方法中,不可访问实例的持久状态和关系。

    实例可以通过伴随实例的EntityContext对象引用的getPrimaryKey、getEJBLocalObject或getEJBObject等方法取得Entity对象的标识。

  • 实现ejbPassivate方法。

    当容器决定清除实例的对象标识时,容器将调用此方法,并将实例返回到实例池中。开发者可以在此方法中为实例在就绪状态中占用的资源进行释放,通常这些资源在ejbActivate方法中分配。在此方法中,不可访问实例的持久状态和关系。

    实例仍然可以通过伴随实例的EntityContext对象引用的getPrimaryKey、getEJBLocalObject或getEJBObject等方法取得Entity对象的标识。

  • 实现ejbRemove方法。

    当客户端调用组件的Home接口或组件接口中的remove方法,或者作为级联移除的结果,容器将调用实例的ejbRemove方法。当ejbRemove方法结束,实例转移到实例池的状态。

    在实例对应的持久业务数据被清除之前,开发者可以在此方法中进行必要的操作。容器在调用ejbRemove方法之前,将会对实例状态与持久存储中的业务数据进行同步。

    ejbRemove方法的事务上下文决定于引发ejbRemove方法的对应remove方法的事务属性。在此方法中,实例仍然可以通过伴随实例的EntityContext对象引用的getPrimaryKey、getEJBLocalObject或getEJBObject等方法取得Entity对象的标识。

    当ejbRemove方法返回,容器将在同一个事务上下文中,将实例从参与的所有容器管理关系从持久存储中移除。

    在此方法执行完成之后的实例状态与实例钝化后的状态相同,准备进入实例池,因此,实例必须释放通常在ejbPassivate方法中释放的资源。

  • 实现ejbLoad方法。

    当容器决定同步实例状态与持久数据时,容器将调用此方法。在ejbLoad方法被调用之前,容器已经将持久数据装入,因此,开发者需要在此方法中对依赖于持久数据的实例变量进行重新计算和初始化。例如,开发者可以通过ejbLoad方法,对通过getter访问方法返回的值进行重新计算,如文本解压缩或进行解密操作。

    执行ejbLoad方法的事务上下文取决于引发ejbLoad的业务方法的事务属性。

  • 实现ejbStore方法。

    当容器决定同步持久数据与实例状态时,容器将调用此方法。在同步实例状态之前,开发者应该使用域访问方法(getter、setter)更新实例,如在使用ejbStore存储数据到持久存储之前,将某文本域的值进行压缩等。

  • 实现ejbFind方法。

    对于容器管理持久性的Entity Bean,开发者不必编写ejbFind方法的内容。只需使用EJB QL查询语言在部署描述中指定查询语句。有关EJB QL查询语言,参见 第 44.7 节 “EJB QL”。有关finder方法的具体内容,参见第 44.6.2.6 节 “Finder方法”

  • 实现ejbHome方法。

    当客户端通过Home接口引用调用Home方法时,容器将选择一个实例并调用对应的ejbHome方法。当容器调用实例的ejbHome方法时,实例处于实例池中,在方法调用返回之后,实例又回到实例池中。

    执行ejbHome方法的事务上下文取决于对应的Home接口中Home方法的事务属性。

    开发者必须在ejbHome方法中提供对Home接口中定义的Home方法的实现。在此方法中,不可访问实例的持久状态和关系。

  • 实现ejbSelect方法。

    开发者可以在组件类中定义零到多个ejbSelect方法。ejbSelect方法并不暴露在Home接口和组件接口中,对客户端是不可见的。通常,开发者通过业务方法调用ejbSelect方法。

    ejbSelect方法必须被定义为抽象(abstract)的。

    ejbSelect方法由开发者在部署描述中指定的信息决定,开发者不需要对ejbSelect方法进行编码。

    定义ejbSelect方法的语法参见第 44.7 节 “EJB QL”。有关select方法的具体内容,参见第 44.6.2.7 节 “Select方法”

    执行ejbSelect方法的事务上下文取决于调用ejbSelect方法的业务方法的事务上下文。

44.6.3. 必须遵守的规则

本节提供对开发容器管理持久性的Entity Bean过程中必须遵循的一些规则。

44.6.3.1. 类与接口

开发者必须提供如下类文件:

  • Entity Bean组件类;

  • Primary Key类文件;

  • 如Entity Bean提供客户端远程访问,必须提供远程接口和远程Home接口;

  • 如Entity Bean提供客户端本地访问,必须提供本地接口和本地Home接口;

44.6.3.2. 组件类

开发者在编写组件类时必须遵守如下规则:

  • 组件类必须实现javax.ejb.EntityBean接口;

  • 类必须声明为公有(public)类,并且必须是抽象(abstract)类;

  • 类不能定义finalize方法;

  • 类可以实现Enttiy Bean的组件接口。如果类实现了组件接口,则类必须提供组件接口中定义的方法的无操作(no-op)实现 ,因为在实例的生存周期内,容器将不会调用这些方法;

  • 类必须提供业务方法、ejbCreate方法、ejbPostCreate方法。

  • 在组件的Home接口中定义的Home业务方法,开发者必须在组件类中提供对应的ejbHome方法实现。当实例在实例池状态时,可以调用这些方法。

  • 组件类中必须提供对Bean的抽象持久模式(abstract persistent schema)的get和set访问方法,这些方法必须被声明为抽象(abstract)的。

  • 组件类可以继承某个类或接口,如果组件类继承某个类,则业务方法、ejbCreate方法、ejbPostCreate方法等从javax.ejb.EntityBean接口继承的方法实现,可以在组件类或组件类继承的类中提供。

  • 组件类不实现finder方法。finder方法由容器提供实现。

  • 必须实现抽象(abstract)的ejbSelect方法。

44.6.3.3. 辅助类

对于Entity Bean的辅助类,必须遵循如下规则:

  • 类必须被定义为公有(public)类 ,并且不能被定义为(abstract)类;

  • 类必须可串行化。

44.6.3.4. ejbCreate方法

Entity Bean的Home接口中定义的create方法,组件类中必须提供对应的ejbCreate方法。Entity Bean可有零到多个ejbCreate 方法,开发者必须遵循如下ejbCreate方法的签名规则:

  • 方法名必须由“ejbCreate”开头;

  • 方法必须是公有的(public);

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

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

  • 如果ejbCreate方法对应于远程Home接口中定义的create方法,则方法参数和值类型必须是合法的RMI类型;

  • 抛出子句必须包含对javax.ejb.CreateException异常的声明,可包含任意应用级异常。

44.6.3.5. ejbPostCreate方法

对每个ejbCreate 方法,开发者必须提供对应的ejbPostCreate方法实现,并遵循如下规则:

  • 方法名必须由“ejbPostCreate”开头;

  • 方法必须是公有的(public);

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

  • 返回值类型必须是void类型;

  • 必须与对应的ejbCreate方法具有同样的参数;

  • 抛出子句可包含任意应用级异常声明,包括javax.ejb.CreateException异常的声明。