用EJB开发订阅信息系统

  作者:板桥banq

上页

5.3.3  CMP图形开发

使用JBuilder的EJB图形化开发工具可以直接将数据库中数据表映射成CMP,前提E两个:首先在JBuilder中安装了支持具体J2EE产品的开发包,如果是JBoss,就要安装上面提到的OpenTool插件;其次要将JBuilder直接访问数据库,如果数据库是MySQL,那么就要按照前面讨论步骤安装MySQL的JDBC包。具体步骤如下。
新建一个EJB MODULE,如果JBuilder的EJB MODULE是灰色的,说明没有安装EJB支持,请检查前面JBuilder和JBoss的配置步骤是否正确。
完成新建EJB MODULE后,单击EJB MODULE,在屏幕的左下方窗口中选择Datasource,按右键,选择import Schema From Database,会弹出页面:Specify a JDBC datasource,在Driver中选择com.mysql.jdbc.Driver(这是在前面配置时输入的),依次输入username和password。
确定OK后,会在Datasource出现MySQL数据库下的3个表,如图5-7所示,选中这3个表,按右键。
ejb
图5-7  JBuilder导入MySQL数据库
选择Create CMP 2.0 Entity Bean,这样3个数据表的CMP就创建成功了,如图5-8所示。
ejb
图5-8  CMP实体Bean
在CMP中,有两个重要技术支撑CMP整个技术规范:EJB-QL和CMR。前者用于实现一些特殊的查询语句。例如非主键关键字查询、全部查询等;而CMR则是代表数据表之间的一对多、一对一或多对多等关系。
在本例中,Customer需要增加根据lastName和firstName的查询,这需要自己加入EJB-QL语句:
SELECT OBJECT(s) FROM Customer AS s WHERE s.firstName=?1
其中Customer是EJB的abstract-schema-name,一般默认abstract-schema-name设定和ejb-name是一致的,只要替换这个EJB-QL中的Customer和lastName就可以实现自己的数据库查询。有关EJB-QL参考http:// www.ejb-ql.com/ 。
其中?1表示查询方法第一个输入参数,如果有第二个输入参数,如middlename,那么EJB-QL语句:
SELECT OBJECT(s) FROM Customer AS s WHERE s.firstName=?1 AND s.middleName=?2
在JBuilder图形开发工具中增加EJB-QL的方法如下:
首先右击Customer,选择Add Finder,按如图5-9所示输入参数和EJB-QL语句,Finder name是可以自己取名,一般以findBy开头;Return type为查询后返回类型,如果预期查询结果可能不只是一个数据对象,而是多个数据集合,那么在这里选择Collection;Input parameters是这个方法的输入参数,这个输入参数实际是EJB-QL的查询关键字,它将和EJB-QL的?1这样的标识符对应。
ejb
图5-9  EJB-QL
本项目3个表之间是一对多或多对多的关系,需要使用CMR来实现。CMR是有方向的:unidirectional(单向) 和 bidirectional(双向);CMR关系种类有one-to-one,one-to-many,and many-to-many。下面具体演示使用JBuilder图形开发工具来实现CMR。
Customer和Address之间是一对多关系,每个Customer有可能有多个Address,选择Customer,选择Add Relationship,指向Subscription,如图5-10所示。
ejb
图5-10  CMR
在图5-10中右下方,有Edit Table Reference按钮,单击该按钮进入数据表,关系建立,将数据表Customer的字段id和Address的字段customerID建立联系,单击这两个字段中之一,然后拖动到另外一个表的字段,这样,数据表之间的一对多关系也建立完成。
在CustomerBean类中将自动增加如下两种CMR方法:
public abstract Collection getAddresses(); 
public abstract void setAddresses (Collection addresses);
Customer和Subscription之间是多对多关系,一个Customer可能订阅几种刊物Subscription,而一个Subscription可能被几个Customer订阅,通过上述图形化工具建立多对多关系,不同于图5-10的是Multiplicity为many to many,而Navigability为bidirectional,
在多对多关系的Edit Table Reference中,一般需要有一个中间数据表cross table,用来维持两个多对多关系表的ID。本例中,这个关系数据表结构如下:
CREATE TABLE customer_subscriptions_subscription_customers (
  Customer varchar(250) binary NOT NULL default '',
  Subscription varchar(250) binary NOT NULL default '',
  PRIMARY KEY  (Customer,Subscription)
)
这个表可以由JBuilder自动创建或自己手工创建。
在CustomerBean类中又将自动增加如下两种CMR方法:
public abstract void setSubscriptions(java.util.Collection subscriptions);
public abstract java.util.Collection getSubscriptions();
由于CMR方向是双向的(bidirectional),在SubscriptionBean中将也有两个CMR方法:
public abstract void setCustomers(java.util.Collection customers);
public abstract java.util.Collection getCustomers();
这样,Customer有两种CMR:与Address的一对多关系以及与Subscription的多对多关系。当客户端查询获取一个Customer数据时,在EJB内部实际执行了以下SQL语句:
SELECT firstName, lastName FROM customer WHERE (id=?)
这是根据客户端的id查询customer数据表,然后执行下列语句:
SELECT addressID FROM address WHERE (customerID=?)
上句SQL的执行可以看成是CustomerBean中getAddresses()方法驱动的。这句只是查询CMR关系,由于数据库设计时,将Customer和Address一对多关系设计在Address表中,也就是说,Address表中的csutomerID和addressID代表了一对多关系,但是从上面EJB实现JDBC语句来看,应该将这种一对多关系单独建立一个表,这样EJB执行上面语句时,效率和性能要高得多。
customer与subscription的多对多关系是通过下列语句获得:
SELECT Subscription FROM customer_subscriptions_subscription_customers WHERE (Customer=?)
这是查询customer_subscriptions_subscription_customers表。这条SQL语句执行可以看成是CustomerBean中的getSubscriptions()方法驱动的。
设定完成实体Bean的CMR,使用CMR是非常方便的。例如将一个Subscription加入到Customer中,使之成为Customer订阅的一个刊物,那么调用语句如下:
//获得CMP customerLocal
CustomerLocal customerLocal = chome.findByPrimaryKey(id);
//获得CMP subscriptionLocal
SubscriptionLocal subscriptionLocal = shome.findByPrimaryKey(title);
//将subscriptionLocal加入customerLocal
customerLocal.getSubscriptions().add(subscriptionLocal);
删除一种CMR关系也很简单,如下:
CustomerLocal customerLocal = chome.findByPrimaryKey(id);
SubscriptionLocal subscriptionLocal = shome.findByPrimaryKey(title);
customerLocal.getSubscriptions().remove(subscriptionLocal);
这样,就解除了这个Customer和某个特定的Subscription之间的多对多关系。

5.3.4  实体Bean

通过上述JBuilder的EJB图形开发工具,基本完成实体Bean的开发工作。JBuilder的图形工具自动生成了Java代码和配置文件。以Customer为例,图形工具生成了EJB必备的3个类:
Home接口CustomerHome
这是一个EJB Home接口,每个EJB都有一个home接口,这个home 接口必须继承EJBHome。
这个 Home接口主要是供调用者调用的。调用者首先通过JNDI寻找到这个Home接口,然后就可以通过这个Home接口实现EJB对象的创建create、寻找find或删除remove等操作。
下面是CustomerHome代码,这是JBuilder图形工具自动生成的:
package com.jdon.samples.ejb;
import javax.ejb.*;
import java.util.*;
public interface CustomerHome extends javax.ejb.EJBLocalHome {
  //创建方法
  public CustomerLocal create(String id) throws CreateException;
  //以下是寻找方法
  public CustomerLocal findByFN(String firstName) throws FinderException;
  public CustomerLocal findByLN(String lastName) throws FinderException;
  public CustomerLocal findByPrimaryKey(String id) throws FinderException;
}
本地接口CustomerLocal

这是一个本地接口,在EJB2.0中,增加了本地接口的支持。在EJB2.0之前,通常这是一个远程Remote接口。如果确认这个EJB与调用者位于同样的JVM,那么使用本地接口会提高速度,从而消除了网络潜在的问题、参数复制的问题以及需要与Java RMI-IIOP兼容的问题。
在本地/远程接口中,定义了这个EJB的业务方法。也就是说,这个接口和下面的Bean实现之间的关系,就如同Java一般使用中接口和实现子类的关系。在Java平时使用中,总是定义一个接口interface,然后由子类继承实现,如:
public inteface CustomerOP{
   public void getId();
}
public class CustomerOPimp implements CustomerOP{
public void getId(){
   …
}
}


即在本地/远程接口中,是用户自己开发的方法,这些具体方法将在CustomerBean中实现。这个本地/远程接口必须继承的是EJBLocalObject/EJBObject。
CustomerLocal的代码如下:
public interface CustomerLocal extends javax.ejb.EJBLocalObject {
  public String getId();
  public void setFirstName(String firstName);
  public String getFirstName();
  public void setLastName(String lastName);
  public String getLastName();
  public void setAddresss(Collection addresss);
  public Collection getAddresss();
  public void setSubscriptions(Collection subscriptions);
  public Collection getSubscriptions();

  public Customer getCustomer();
}
在这个本地接口中,主要定义了一些set或get方法,这是一个典型的CMP实体Bean的方法,这些set或get方法最终实际是由EJB容器来实现的。
Bean实现CustomerBean
不同于上面两个接口,这是一个真正的类。在这个类中实现了上面本地/远程接口的具体方法,如果这个EJB是一个Session Bean,那么就由开发者自己编入代码实现。
在CMP中,这是一个抽象类,即具体实现是由容器实现的,CustomerBean代码如下:
abstract public class CustomerBean implements EntityBean {
  EntityContext entityContext;
  public java.lang.String ejbCreate(java.lang.String id) throws CreateException {
    setId(id);
    return null;
  }
  public void ejbPostCreate(java.lang.String id) throws CreateException {  }
  public void ejbRemove() throws RemoveException {  }
  public abstract void setId(java.lang.String id);
  public abstract void setFirstName(java.lang.String firstName);
  public abstract void setLastName(java.lang.String lastName);
  public abstract void setAddresss(java.util.Collection addresss);
  public abstract void setSubscriptions(java.util.Collection subscriptions);
  public abstract java.lang.String getId();
  public abstract java.lang.String getFirstName();
  public abstract java.lang.String getLastName();
  public abstract java.util.Collection getAddresss();
  public abstract java.util.Collection getSubscriptions();
  public void ejbLoad() { }
  public void ejbStore() { }
  public void ejbActivate() {  }
  public void ejbPassivate() {  }
  public void unsetEntityContext() {
    this.entityContext = null;
  }
  public void setEntityContext(EntityContext entityContext) {
    this.entityContext = entityContext;
  }
  //获得CMR相关对象,这是CMP特有的方法
  public Customer getCustomer() {
    Customer customer = new Customer();
    customer.setId(getId());
    customer.setFirstName(getFirstName());
    customer.setLastName(getLastName());
    //获得一对多关系的Address数据
    Collection aResult = new ArrayList();
    Iterator iter0 = getAddresss().iterator();
    while (iter0.hasNext()) {
      AddressLocal addresslocal = (AddressLocal) iter0.next();
      aResult.add(addresslocal.getData());
    }
    customer.setAddresss(aResult);
    //获得一对多关系的Subscription数据
    Collection sResult = new ArrayList();
    Iterator iter = getSubscriptions().iterator();
    while (iter.hasNext()) {
      SubscriptionLocal subscriptionlocal = (SubscriptionLocal) iter.next();
      sResult.add(subscriptionlocal.getData());
    }
    customer.setSubscriptions(sResult);
    return customer;
  }
}
这个CustomerBean其实也没有真正实现set或get方法,这些交给EJB容器实现了。
如果这个EJB是一个BMP,也就是说,必须由Bean自己来实现这些set或get方法。这也是取名BMP的原因(Bean 管理的持久性),那么CustomerBean就不是这样写法。
虽然在日常开发中,推荐优先使用CMP,但是在下列一些情况下,BMP还是需要使用的。如使用旧的只支持EJB 1.1规范的EJB容器;或需要复杂查询,无法使用CMP的EJBQL完成的等。
为了实现BMP,需要记住BMP几个方法的意义,而这些方法在CMP中基本无需关注,这些方法是:
ejbLoad()方法:类似SQL的Select语句,从数据库中获取数据
ejbStore()方法:类似SQL的Update语句,将数据更新保存到数据库中
ejbRemove()方法:类似SQL的Delete语句,从数据库中删除数据
ejbCreate()方法:类似SQL的Insert语句,向数据库插入一条数据记录
ejbFindByPrimaryKey(primary key)方法:以primary key 查询数据库
以CustomerBean为例,如果需要将CustomerBean变成一个BMP的实体Bean。那么就必须使用SQL语句实现数据库的相关操作。只要创建一个继承CustomerBean 的子类CustomerBeanBMP就可以,如下:
public class CustomerBeanBMP extends CustomerBean{
public String id = "";
public String firstName = "";
public String lastName = "";

public String getId(){  return id}
public void setId(String Id){  this.Id = Id; }
public String getFirstName(){  return firstName}
public void setFirstName (String firstName){  this. firstName = firstName; }
public String getLastName(){  return lastName }

public void setLastName (String lastName){  this. lastName = lastName; }

}
以EJBCreate方法为例,具体实现如下:
public String ejbCreate(String id) throws CreateException {
   try {
     Connection connection = getConnection();
     PreparedStatement statement = connection.prepareStatement
      ("INSERT INTO customer (id, firstName, lastName) VALUES (?, ?, ?)");
     statement.setString(1, id);
     statement.setString(2, this.firstName);
     statement.setInt(3, this,lastName);
     if (statement.executeUpdate() != 1) {
      statement.close();
      connection.close();
      throw new CreateException("Could not create: " + item);
     }
     statement.close();
     connection.close();
     return item;
   }
   catch(SQLException e) {
     throw new EJBException("Could not create: " + item, e);
   }
}
其中getConnection()是直接获得容器的连接池,这是通过JNDI寻找的,代码如下:
private Connection getConnection() throws SQLException {
   DataSource ds = null;

   try {
      Context ctx = new InitialContext();
      ds = (DataSource) ctx.lookup ("java:comp/env/jdbc/CustomerDB") ;
   } catch (NamingException exp) {
      exp.printStackTrace() ;
   }
   return (ds == null) ? null : ds.getConnection();
}
这样,一个CustomerBeanBMP基本完成。仔细研究会发现,这个CustomerBeanBMP非常类似“简单的用户注册系统”中的Profile.java等JavaBeans,所不同的只是每个操作方法名称必须依据EJB规定使用特定的名称,如ejbCreate表示创建数据等。
在getConnection()方法中设置了DataSource的JNDI名称java:comp/env/jdbc/ CustomerDB,java:comp/env是J2EE规定的一种对reference调用的写法,正如在Web层如果使用java:comp/env/jdbc/CustomerDB来定位数据库连接池时,需要在web.xml中定义jdbc/CustomerDB资源引用一样,在EJB层,也需要在配置文件中定义jdbc/CustomerDB资源引用,如下:
<resource-ref>
      <description />
      <res-ref-name>jdbc/CustomerDB</res-ref-name>
      <res-type>javax.sql.DataSource</res-type>
      <res-auth>Container</res-auth>
</resource-ref>
上面是定义在部署描述文件ejb-jar.xml,类似数据库连接池在web.xml中的配置,在web.xml中,只有这次定义就已足够,但是在EJB层中,还需要在与具体容器相关的配置文件中,将jdbc/CustomerDB和容器提供的JNDI名称对应起来,如下:
<entity>
      <ejb-name>Customer</ejb-name>
      <local-jndi-name>Customer</local-jndi-name>
      <resource-ref>
           <res-ref-name>jdbc/CustomerDB</res-ref-name>
           <jndi-name>java:/ CmpSUBDS</jndi-name>
      </resource-ref>
</entity>
上面配置必须写入与具体EJB产品相关的配置文件中,如果是JBoss,那么应该写入jboss.xml配置文件中,其中jndi-name是JBoss容器配置的数据库连接池JNDI名称,那么如何配置JBoss容器的数据库连接池JNDI名称,可见后面章节的JBoss和MySQL配置。
从后面章节的配置讨论中可以知道,这个JNDI名称是CmpSUBDS,按照JBoss的JNDI名称写法要求,在jboss.xml中写法不是直接写入CmpSUBDS,而是要加入前缀java:/。
上面讨论了使用BMP时的配置文件的写法,在EJB中,配置文件编写已经和代码编写混同在一起,具有同等重要性,特别是对于严重依赖容器的CMP,专门有配置jbosscmp-jdbc.xml用来描述CMP字段和数据表字段之间的关系以及CMR关系等。
所幸的是,JBuilder图形开发工具已经完成了jbosscmp-jdbc.xml文件的编写,jbosscmp-jdbc.xml主要由3部分定义组成:
第一部分是CMP数据库的Datasource,与BMP的 Datasource是在jboss.xml中逐个配置不同的是,CMP数据库连接池的JNDI是在jbosscmp-jdbc.xml中逐个配置,如下:
<defaults>
        <datasource>java:/CmpSUBDS</datasource>
        <datasource-mapping>mySQL</datasource-mapping>
</defaults>
这两行是最基本的Datasource配置,更多配置可见JBoss CMP手册。
第二部分是实体Bean和数据表字段对应,如下:
<enterprise-beans>
        <entity>
            <ejb-name>Address</ejb-name>
            <table-name>address</table-name>
            <cmp-field>
                <field-name>addressID</field-name>
                <column-name>addressID</column-name>
            </cmp-field>
            <cmp-field>
                <field-name>street</field-name>
                <column-name>street</column-name>
            </cmp-field>
            ……
         </entity> 

</enterprise-beans>
这是为了告诉EJB容器,实体Bean字段和哪些数据表字段相联系。这是必需的,否则容器无法代为执行数据表的增加、删除、修改和查询等操作。
第三部分是CMR的定义,定义实体Bean之间的一对一等对应关系,部分代码如下:
<relationships>
        <ejb-relation>
            <ejb-relation-name>customer-address</ejb-relation-name>
            <foreign-key-mapping />
            <ejb-relationship-role>
                <ejb-relationship-role-name>
                   CustomerRelationshipRole
                </ejb-relationship-role-name>
                <key-fields>
                    <key-field>
                        <field-name>id</field-name>
                        <column-name>id</column-name>
                    </key-field>
                </key-fields>
            </ejb-relationship-role>
            …
        </ejb-relation>
</relationships>
当然,不同的EJB产品,jbosscmp-jdbc.xml配置文件名不一样,Weblogic有自己的CMP配置文件名,具体可参考Weblogic手册。
除了CMP的配置文件jbosscmp-jdbc.xml外,还有一个与具体容器产品相关的配置文件jboss.xml。前面已经提及,jboss.xml主要是解决JNDI和EJB Reference之间的对应关系,这将在后面章节JNDI配置中讨论。
ejb-jar.xml是EJB规定的基本配置文件,称为部署发布描述器,主要是对EJB进行详细的描述。如是Session Bean还是Entity Bean;是他们之中具体哪一个类型;如果一个EJB要调用另外一个EJB,那么就在这个EJB中指明被调用EJB的引用reference,这样调用者EJB才能找到被调用者EJB。
如果这个EJB是实体Bean CMP,那么ejb-jar.xml中关于这个EJB的描述会更多,下面是实体Bean Customer的描述:
<entity>
        <display-name>Customer</display-name>
        <ejb-name>Customer</ejb-name> <!--  EJB名称,用于被外界调用 -->
        <!--  下面是EJB的3个基本接口或类 -->
        <local-home>com.jdon.samples.ejb.CustomerHome</local-home>
        <local>com.jdon.samples.ejb.CustomerLocal</local>
        <ejb-class>com.jdon.samples.ejb.CustomerBean</ejb-class>
        <!-- 定义为容器实现持久化 如果是BMP 这里为Bean -->
        <persistence-type>Container</persistence-type>
<!-- 定义了该entity bean的主键类型 -->
        <prim-key-class>java.lang.String</prim-key-class>
        <reentrant>False</reentrant>
        <!—以下是CMP特有的定义,如果是BMP,下面只要定义resource-ref -->
        <cmp-version>2.x</cmp-version>
        <abstract-schema-name>Customer</abstract-schema-name>
        <cmp-field>
                <field-name>id</field-name>
        </cmp-field>
        <cmp-field>
                <field-name>firstName</field-name>
        </cmp-field>
        <cmp-field>
                <field-name>lastName</field-name>
        </cmp-field>
        <primkey-field>id</primkey-field>
<!--  以下是EJB QL语法 -->
         <query>
                <query-method>
                    <method-name>findByFN</method-name>
                    <method-params>
                        <method-param>java.lang.String</method-param>
                    </method-params>
                </query-method>
                <ejb-ql>SELECT OBJECT(s) FROM Customer AS s
                         WHERE s.firstName=?1
                </ejb-ql>
        </query>
            …
</entity>
在ejb-jar.xml都是这样对每个EJB进行描述的XML代码。对于Session Bean来说,ejb-jar.xml描述要相对简单得多。这些配置也是JBuilder图形工具自动生成的,无需手工逐行输入。对于开发者只要大概了解它们的含义就行。最经常需要配置的是JNDI和reference配置,一旦配置不正常,EJB就无法调用运行,这在后面章节会具体讨论。
至此,我们已经知道,EJB是由下列几个部分组成:
一个Home接口;一个本地/远程接口;一个真正Bean实现;ejb-jar.xml部署描述器,对EJB具体特征和引用reference进行描述;还有两个与具体EJB产品相关的配置文件,JBoss是jboss.xml和jbosscmp-jdbc.xml,前者主要解决reference和具体EJB产品的JNDI之间的对应关系,后者主要用于实体Bean的CMP。
EJB虽然复杂,但是掌握了这几个部分,就基本可以掌握EJB的开发和应用了。

 

下页