作者:自由的猪 制作整理:
左岸网络
http://www.leftworld.net第6章 CMP的例子
Dale Green 著
Iceshape Zeng 译
CMP实体Bean技术给开发者带来了很多重要的好处。首先,EJB容器处理了所有数据库的访问操作。其次,容器管理了实体Bean之间的关系。因为这些服务,你不需要为数据库访问而编写任何代码,而只用在部署描述符里指定配置。这种方法不仅可以节省开发者的时间,更重要的是它使企业Bean在访问不同的数据库时有更好的可移植性。
本章将重点介绍CMP实现的实体Bean应用程序RosterApp这个例子的代码和部署描述符设置。如果你对本章提到的术语和概念不太熟悉,请参考第4章企业Bean的CMP部分。
本章内容:
RosterApp应用概述
PlayerEJB代码分析
实体Bean类
Local Home接口
Local接口
RosterApp设置说明
RoseterApp应用程序设置
RosterClient客户端设置
RosterJAR设置
TeamJAR设置
RosterApp中的方法调用
创建一个Player实体
将Player加入一个Team实体
删除一个Player
从Team中删除一个Player
查询一个Team中所有的Player
得到Team中所有Player的副本
查询特定位置的Player
查询一个Player参加的Sports
运行RosterApp应用程序
启动用到的程序
部署
运行客户端
用deploytool工具部署CMP实现的实体Bean
指定企业Bean类型
选择持久性字段和抽象模式名
为查找方法和Select方法编写EJB QL查询
生成SQL语句和指定表创建机制
设置数据库的JNDI名,用户名和密码
定义关系
CMP主键
主键类
实体Bean的主键
产生主键值
一、RosterApp应用概述
RosterApp应用程序维护运动社团中运动员分组的花名册。它有五个组成部分,RosterAppClient是通过Remote接口访问会话Bean RosterEJB的J2EE客户端,RosterEJB通过Local接口访问三个实体Bean:PlayerEJB,TeamEJB和LeagueEJB。
这些实体Bean用容器管理的持久性和关系。TeamEJB和PlayerEJB之间是双向的多对多关系。双向关系中每一个Bean都有一个关系字段来确定相关联另一个Bean实例。多对多关系是指:可以参加多个运动项目的运动员(Player)可以加入多个组(team),而每个组又有多个运动员。LeagueEJB和TeamEJB之间是一对多的双向关系:一个社团可以有多个组,而一个组只能属于一个社团。
图6-1描述了该应用程序各组件和它们之间的关系。虚线箭头表示通过调用JNDI lookup方法的访问。
图 6-1 RosterApp应用程序
二、layerEJB代码分析
PlayerEJB表示运动社团中的运动员实体。本例中,它需要以下三各个类:
1. 实体Bean类(PlayerBean)
2. Local Home接口(LocalPlayerHome)
3. Local接口(LocalPlayer)
你可以在本例的源代码文件存放目录j2eetutorial/examples/src/ejb/cmproster中找到以上代码的源文件,要编译这些源文件,在命令方式下进入j2eetutorial/examples目录,执行antcmproster命令。RosterApp.ear的样本文件存放在j2eetutorial/examples/ears目录下。
实体Bean类
CMP实现的实体Bean必须符合CMP的语法规则。首先Bean类必须定义为公有(public)和抽象(abstract)的。其次要实现一下内容:
1. EntityBean接口
2. 零对或多对ejbCreate和ejbPostCreate方法
3. 持久性字段和关系字段的get和set方法定义为abstract
4. 所有的select方法定义为abstract
5. 商业方法
CMP的实体Bean类不能实现如下方法
1. 查找方法
2. finalize方法
CMP和BMP实现实体Bean的代码比较
因为CMP不需要编写数据库访问的代码,所以CMP实现的实体Bean比BMP实现的实体Bean的代码少得多。例如本章中讨论的PlayerBean.java源文件要比第5章的代码文件SavingsAccountBean.java小得多。下表比较了两种不同类型实体Bean实现代码的不同:
表6-1两种持久性机制的编码比较
不同点
CMP
BMP
企业Bean类定义
抽象
非抽象
数据库访问代码
由工具产生
开发者编码
持久性状态
虚拟持久字段表示
代码中的实例变量表示
持久性字段和关系字段的访问方法
必须
不是必须
findByPrimaryKey方法
由容器处理
开发者编码
其他查找方法
容器根据开发者定义的EJB QL查询自动处理
开发者编码
select方法
容器处理
不需要
ejbCreate方法的返回值
应该为null
必须是主键类
注意:对于两种持久性机制,商业方法和Home方法的实现规则都是一样的。参考第5章的商业方法和Home方法两节。
访问(get和set)方法
CMP实现的实体Bean中持久性字段和关系字段都是虚拟的,你不能把它们在Bean类中写成实例变量,而应该在部署描述符里列出它们。要访问这些字段,你需要在实体Bean类中定义抽象的get和set方法。
持久性字段的访问方法
EJB容器根据部署描述符信息自动为持久性字段实现存储到数据库和从数据库读取操作。(具体的操作过程可能是在执行部署的同时,部署工具就根据部署描述符的信息生成了对应的数据库访问代码)本例中PlayerEJB的部署描述符中定义了以下持久性字段:
☆ playerId(primary key)
☆ name
☆ position
☆ salary
PlayerBean类中以上字段的访问方法定义如下:
public abstract String getPlayerId();
public abstract void setPlayerId(String id);
public abstract String getName();
public abstract void setName(String name);
public abstract String getPosition();
public abstract void setPosition(String position);
public abstract double getSalary();
public abstract void setSalary(double salary);
访问方法名以get或者set开头,后跟头字母大写的对应持久性字段名或者关系字段名。例如字段salary的访问方法命名为:getSalary和setSalary。这种命名约定和JavaBean组件的相同。
关系字段的访问方法
在RosterApp应用程序中,因为一个运动员可以属于多个组,一个PlayerEJB实例可以关联多个TeamEJB实例。为了说明这个关系,部署描述符中定义了一个名叫teams的关系字段。在PlayerBean类中,teams的访问方法定义如下:
public abstract Collection getTeams();
public abstract void setTeams(Collection teams);
select方法
select方法和查找方法有很多相同的地方:
☆ select方法可以返回Local或者Remote接口或者它们之一的集合
☆ select方法访问数据库
☆ 部署描述符为每个select方法指定EJB QL查询
☆ 实体Bean类并不实现select方法(只是声明或者说定义)
当然,select方法和查找方法还有一些显著的区别:
1. 一个查找方法可以返回相关联实体Bean的一个持久性字段或者字段的集合。而查找方法值可以返回所属实体Bean的Local或者Remote接口或者它们之一的集合。
2. 因为select方法在Local或者Remote接口中没有被定义,所以它们不能被客户端访问,而只能被所属实体Bean中实现的方法调用(用点像类的私有方法)。select方法通常被商业方法调用。
3. select方法在实体Bean类里定义。BMP的查找方法也在实体Bean类中定义,但是CMP的查找方法不在实体Bean类里定义。CMP中的查找方法在Home或者Local Home接口中定义,在部署描述符中定义对应的EJB QL查询。
PlayerBean类定义了下面这些select方法:
public abstract Collection ejbSelectLeagues(LocalPlayer player)
throws FinderException;
public abstract Collection ejbSelectSports(LocalPlayer player)
throws FinderException;
select的方法签名规则:
1. 方法名要以ejbSelect开头
2. 访问修饰符必须是public
3. 方法必须声明为抽象的
4. throws子句必须包括javax.ejb.FinderException
商业方法
因为客户端不能调用select方法,所以PlayerBean类将它们封装(wraps)在商业方法getLeagues和getSports中:
public Collection getLeagues() throws FinderException {
LocalPlayer player = (team.LocalPlayer)context.getEJBLocalObject();
return ejbSelectLeagues(player);
}
public Collection getSports() throws FinderException {
LocalPlayer player = (team.LocalPlayer)context.getEJBLocalObject();
return ejbSelectSports(player);
}
实体Bean方法
因为处理CMP实体Bean的持久性,PlayerBean的生命周期方法都接近于空方法了。ejbCreate方法将参数赋值给持久性字段以初始化实体Bean实例。ejbCreate方法调用结束后,容器向数据库对应表中插入一行。ejbCreate方法实现:
public String ejbCreate (String id, String name,
String position, double salary) throws CreateException {
setPlayerId(id);
setName(name);
setPosition(position);
setSalary(salary);
return null;
}
ejbPostCreate方法必须和对应的ejbCreate方法有相同的参数签名和返回值。如果你想在初始化Bean实例时设置关系字段值,可以实现ejbPostMethod方法,而不可以在ejbCreate方法中设置关系字段值。
除了一个调试语句,PlayerBean的ejbRemove方法就是一个空方法了。容器在删除数据库表中对应行之前调用ejbRemove方法。
容器自动同步实体Bean状态和数据库数据。容器从数据库中读取实体状态后调用ejbLoad方法,同样的方式,在将实体状态存入数据库前调用ejbStore方法。(这句话有些费解,因为在BMP中数据库的同步是在ejbLoad和ejbStore这两个方法里实现的,难道部署工具或者容器为CMP生成的实现子类不使用这两个方法来实现数据库同步的?)
Local Home接口
Local Home接口定义本地客户端调用的create方法、查找方法和Home方法。
create方法的语法规则如下:
1. 方法名以create开头
2. 和对应的实体Bean类里定义的ejbCreate方法有相同的参数签名
3. 返回实体Bean的Local接口
4. throws子句包括对应ejbCreate方法throws子句中出现的所有异常加上javax.ejb.CreateException异常
查找方法的规则:
1. 方法名以find开头
2. 返回实体Bean的Local接口或它的集合
3. throws子句包括javax.ejb.FinderException异常
4. 必须定义findByPrimaryKey方法
下面是LocalPlayerHome的部分代码:
package team;
import java.util.*;
import javax.ejb.*;
public interface LocalPlayerHome extends EJBLocalHome {
public LocalPlayer create (String id, String name,
String position, double salary)
throws CreateException;
public LocalPlayer findByPrimaryKey (String id)
throws FinderException;
public Collection findByPosition(String position)
throws FinderException;
...
public Collection findByLeague(LocalLeague league)
throws FinderException;
...
}
Local接口
该接口定义了本地客户端调用的商业方法和字段访问方法。PlayerBean类实现了两个商业方法getLeagues和getSports,同时还为持久性字段和关系字段定义了很多get和set访问方法。但是客户端不能调用set方法,因为LocalPlayer接口中没有定义这些set方法。但是get方法客户端可以访问。下面是LocalPlayer的实现代码:
package team;
import java.util.*;
import javax.ejb.*;
public interface LocalPlayer extends EJBLocalObject {
public String getPlayerId();
public String getName();
public String getPosition();
public double getSalary();
public Collection getTeams();
public Collection getLeagues() throws FinderException;
public Collection getSports() throws FinderException;
}
三、RosterApp配置说明
本节将引导你为CMP实现的实体Bean配置部署描述符。在这个过程中,还将讨论deplouytool工具中出现的重要的选项页和对话框。
请先运行deploytool并打开j2eetutorial/examples/ears目录下的RosterApp.ear文件。
RosterApp应用程序配置
在属性视图中选中RosterApp节点以查看应用程序的部署信息。
General页(RosterApp)
Contents域显示了RosterApp.ear中包含的文件,包括两个EJB JAR文件(team-ejb.jar和roster-ejb.jar)和J2EE应用程序客户端JAR文件(roster-ac.jar)。如图6-2:
图 6-2 RosterApp 的General 页
JNDI Names页(RosterApp)
Application表列出了RosterApp应用程序中的企业Bean的JNDI名。
References表有两个条目,EJB Ref条目为RosterClient客户端映射RosterEJB会话Bean的引用名(ejb/SimpleRoster)。Resource条目指定TeamJAR模块中的实体Bean访问的数据库的JNDI名。
RosterClient客户端配置
展开RosterApp节点,选中RosterClient节点,将显示客户端配置信息。
JAR File页(RosterClient)
Contents域显示了roster-ac.jar包含的文件:两个XML文件(部署描述符文件)和一个类文件(RosterClient.class)。
EJB Refs页(RosterCliet)
RosterClient客户端访问一个企业Bean:RosterEJB会话Bean。因为是远程访问,Interfaces列选择Remote,Local/Remote列填写会话Bean的Remote接口(roster.Roster)。
RosterJAR的设置
在树视图中选中RosterJAR节点。该JAR文件包含RosterEJB会话Bean。
General页(RosterJAR)
Contents域列出了三个包:roster包包含RosterEJB需要的类文件——会话Bean类,Remote接口和Home接口;team包包含RosterEJB要访问的实体Bean的Local接口;util包里是应用程序用到的一些实用类(utility classes)。
RosterEJB
展开RosterJAR节点点选RosterEJB节点。
General页(RosterEJB)
本例中General选项页显示RosterEJB是一个远程访问的有状态会话Bean。因为它不支持本地访问,所以Local Interface域为空。
EJB Refs页(RosterEJB)
RosterEJB会话Bean访问三个实体Bean:PlayerEJB、TeamEJB和LeagueEJB。因为这些访问都是本地访问,这些引用条目中的Interfaces列中都定为Local,Home Interface列显示的是这些实体Bean的Local Home接口。Local/Remote Interfaces列显示的是这些实体Bean的Local接口。
选中表中的一行,可以查看运行时部署信息。例如当你选择Coded Name列为ejb/SimpleLeague的一行时,LeagueEJB就显示在Enterprise Bean Name域里。Enterpri Bean Name域需要设置成被引用的企业Bean的名字(在左边树视图中显示的名字)。
TeamJAR配置
点选树视图中的TeamJAR节点。该JAR文件包含三个相互关联的实体Bean:LeagueEJB、TeamEJB和PlayerEJB。
General页(TeamJAR)
Contents域显示了该JAR文件中的两个包:team和util。team包包含三个实体Bean的类文件、Local接口文件和Local Home接口文件。Util包就不再介绍了。
Relationships页(TeamJAR)
如图6-3,CMP实体Bean之间的关系在这个选项页中定义。
图 6-3 TeamJAR的Relationships页
Container Managed Relationships表中定义了两个关系:TeamEJB-PlayerEJB和LeagueEJB-TeamEJB。在TeamEJB-PlayerEJB关系中,TeamEJB被当作EJB A而PlayerEJB被当作EJB B(当然你也可以交换它们的位置,这并不影响关系本身)。
关系编辑对话框(TeamJAR)
在上图所示的Relationship页中选择表中的一行点击Edit按钮,就会弹出关系编辑对话框。(当然Add按钮也可以,只不过不需要选择已有关系,因为……)。如图6-4是TeamEJB-PlayerEJB关系的编辑对话框,下面详细介绍它们的关系。
TeamEJB-PlayerEJB关系
Multiplicity组合框提供四种可选的关系类型(因为这里是单向从A到B,所以一对多和多对一是两种类型,其实可以在下面的EJB组合框中调换关系双方的位置而让它们合成一种)。TeamEJB-PlayerEJB是多对多关系(many to many(*:*)选项)。
Enterprise Bean A框里描述了TeamEJB在关系中的信息。Field Referencing Bean B组合框里选择了TeamEJB中的关系字段(players),这个字段对应TeamBean.java文件中定义的以下两个访问方法:
public abstract Collection getPlayers();
public abstract void setPlayers(Collection players);
图 6-4TeamJAR的关系编辑对话框
FieldType组合框选择的是java.util.Collection以匹配为访问方法中player字段的类型。因为在对TeamEJB在关系中PlayerEJB是“多”的一方(在多对多关系中双方都是“多”),所以player字段的类型是多值对象(集合)。
TeamEJB-PlayerEJB的关系是双向的——关系的双方都有一个关系字段标志关联的实体Bean的实例。如果关系不是双向的,没有关系字段的一方在Field Referenceing组合框中选择<none>。
LeagueEJB-TeamEJB关系
在关系编辑对话框里,LeagueEJB-TeamEJB关系的Multiplicity组合框选择的是One to Many(因为一个社团可以有多个组,而且这里选定后,Enterprise Bean A就只能是LeagueEJB了)。
LeagueEJB中用关系字段teams来表示该社团里所有的组。因为TeamEJB是关系中“多”的一方,所以teams字段是一个集合。而LeagueEJB是“一”的一方,所以TeamEJB中league是单个的LocalLeaguer类型对象。TeamBean.java通过如下访问方法定义关系字段league:
public abstract LocalLeague getLeague();
public abstract void setLeague(LocalLeague players);
在LeagueEJB-TeamEJB关系中定义了关联删除,TeamEJB中Delete When A Is Deleted符选框被选中。这样当一个LeagueEJB实例被删除时,与它相关联的TeamEJB实例也将自动地被删除,这就是关联删除。LeagueEJB中相同的符选框并没有选中,不能删除一个组就删除整个社团,因为还有其他组在社团中。一般在关系中“多”的一方才会被关联删除,另一方不会被自动删除。
PlayerEJB
在树视图中选择PlayerEJB节点(TeamJAR的字节点之一)。
General页(PlayerEJB)
这一页显示企业Bean类和EJB接口。因为PlayerEJB是容器管理关系的靶子,所以它有本地接口(Local和Local Home接口),同时它不需要允许远程访问,所以没有远程接口(Remote和Home接口)。
Entity页(PlayerEJB)
如图6-5,在这一页上面的单选按钮组定义企业实体Bean的持久性类型。为PlayerEJB选定的是container-managed persistence(2.0),就是CMP2.0。因为CMP1.0不提供容器管理的关系服务,所以不推荐使用。(这些版本号是EJB规范的版本号,而不是J2EE SDK的版本号)
Fields To Be Persisted列表框列出了PlayerBean.java中定义了访问方法的所有持久性字段和关系字段。持久性字段前面的选择框必须被选中,而关系字段不能不选中。就是说关系字段(外键)不在本实体Bean中被持久化(这里或许是因为数据库表本身是用参照约束实现的外键关联,所以关系字段不需要在这里持久化,如果数据表关系是用应用程序编码检查来实现,那么这里就应该让关系字段也被持久化,但是这时这个字段还是不是关系字段呢?)。PlayerEJB实体Bean的关系字段只有一个:teams。
abstract schema name是Player,这个名字代表PlayerEJB实体Bean的所有持久性字段和关系字段(抽象模式名标志一个定义持久性字段和关系字段的抽象模式)。这个抽象模式名将在为PlayerEJB定义的EJB QL 查询中被引用。EJB QL将在第8章论述。
图 6-5PlayerEJB的Entity页
Finder/Select Methods对话框(PlayerEJB)
在Entity页点击Finder/Select Methods按钮会打开Finder/Select Methods对话框,如图6-6。你可以在这个对话框里查看和编辑为CMP实体Bean的查找和Select方法写的EJB QL查询。
图 6-6 Finder/Select Methods 对话框
Entity Deployment Settings对话框(PlayerEJB)
在Entity页点击Deployment Settings按钮打开该对话框,该对话框用来设置CMP实体Bean的运行时配置信息。这些信息是J2EE SDK特有的,其他的J2EE平台可能有些不同。在J2EE SDK中,实体Bean的持久性字段存储在关系数据库中,在Database Table框中你可以确定是否让服务器自动创建和删除数据库中的表。如果你想把数据保存在数据库中,就不选中Delete table on undeploy复选框,否则当你从服务器卸载实体Bean时,表将被删除。
J2EE服务器通过SQL语句访问数据库,用CMP实现实体Bean时你不需要自己编写这些SQL语句,点击General Default SQL按钮deploytool工具会自动生成这些语句。然后在Method表格里选中任意一个方法,会在右边的SQL Query域里看到为这个方法生成的SQL语句,你可以在这里修改这些生成的语句。
对查找和Select方法,对应的EJB QL查询也会显示在下面的EJB QL Query域里。在你点击General Default SQL按钮时,deploytool把EJB QL查询转换成SQL语句,如果你修改了EJB QL查询,你应该让deploytool重新生成SQL语句。
点击Container Methods单选按钮,可以看到为容器管理方法生成的SQL语句。如createTable方法的创建表SQL语句,当然还有删除表的SQL语句。
当容器创建一个PlayerEJB实例时,它生成一条插入数据(INSERT)的SQL语句,要查看这条语句,在Method表各种选择createRow方法,这条语句的VALUES子句中的变量是Home接口中create方法的对应参数。
Database Deployment Settings对话框(PlayerEJB)
在Entity Deployment Settings对话框中点击Database Settings按钮打开该对话框。在这里你可以为数据库指定JNDI名,这是非常重要的,因为没有JNDI名你将无法访问数据库。本例中的数据库JNDI名为jdbc/Cloudscape,用户名和密码为空。
四、RosterApp中的方法调用
为了说明组件之间如何相互协作,本节描述实现特定功能时的方法调用顺序。这些组件的源文件在j2eetutorial/examples/src/ejb/cmproster目录下。
“新建”一个运动员
1.RosterClient客户端程序
RosterClient客户端程序调用RosterEJB会话Bean的createPlayer商业方法。在下面的代码中myRoster对象是Roster类型(RosterEJB的远程接口),参数是PlayerDetails对象,该对象封装了一个运动员的所有信息。(PlayerDetails是一个值对象,用来在远程调用间传递实体信息,关于值对象模式的更多信息,参考《J2EE核心模式》一书)。
myRoster.createPlayer(new PlayerDetails("P1", "Phil Jones", "goalkeeper", 100.00));
2.RosetEJB
会话Bean的createPlayer方法创建一个PlayerEJB实体Bean实例。因为PlayerEJB只有本地接口,所以create方法在Local Home接口LocalPlayerHome中定义。下面的代码中playerHome是LocalPlayerHome类型的对象:
public void createPlayer(PlayerDetails details) {
try {
LocalPlayer player = playerHome.create(details.getId(),
details.getName(), details.getPosition(),
details.getSalary());
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
}
3.PlayerEJB
ejbCreate方法用set访问方法将参数值赋给持久性字段,该方法调用后容器生成一个INSERT SQL语句将持久性字段写入数据库:
public String ejbCreate (String id, String name,
String position, double salary) throws CreateException {
setPlayerId(id);
setName(name);
setPosition(position);
setSalary(salary);
return null;
}
将运动员加入组
1.RosterClient客户端
客户端调用RosterEJB的addPlayer方法(参数P1和T1分别代表Player和TeamEJB实例的主键):
myRoster.addPlayer("P1", "T1");
2.RosterEJB
addPlayer方法分两个步骤完成工作:首先它调用findByPrimaryKey查找PlayerEJB和TeamEJB实例;然后直接调用TeamEJB的addPlayer方法。代码如下:
public void addPlayer(String playerId, String teamId) {
try {
LocalTeam team = teamHome.findByPrimaryKey(teamId);
LocalPlayer player =
playerHome.findByPrimaryKey(playerId);
team.addPlayer(player);
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
}
3.TeamEJB
TeamEJB有一个关系字段players,它是属于这个队伍的所有运动员的集合(Collection),它的访问方法如下:
public abstract Collection getPlayers();
public abstract void setPlayers(Collection players);
addPlayer方法县调用getPlayers方法得到关联的LocalPlayer对象的集合,然后调用集合的add方法新加入一个运动员:
public void addPlayer(LocalPlayer player) {
try {
Collection players = getPlayers();
players.add(player);
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
}
“删除”一个运动员
1.RosterClient
删除运动员P4,客户端调用RosterEJH的removePlayer方法:
myRoster.removePlayer("P4");
2.RosterEJB
removePlayer方法调用findByPrimaryKey方法定位倒要删除的PlayerEJB实例然后调用实例的remove方法。这个调用会通知容器在数据库中删除对应的纪录,容器还同时删除相关联的TeamEJB的player关系字段中该实例的引用以更新TeamEJB-PlayerEJB之间的关系。下面是RosterEJB的removePlayer方法:
public void removePlayer(String playerId) {
try {
LocalPlayer player =
playerHome.findByPrimaryKey(playerId);
player.remove();
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
}
从组中开除运动员
1.RosterClient
将运动员P2从组T1中删除的客户端调用:
myRoster.dropPlayer("P2", "T1");
2.RosterEJB
dropPlayer方法的调用和addPlayer很像,她先找到PlayerEJB和TeamEJB的实例,然后调用TeamEJB的dropPlayer方法:
public void dropPlayer(String playerId, String teamId) {
try {
LocalPlayer player =
playerHome.findByPrimaryKey(playerId);
LocalTeam team = teamHome.findByPrimaryKey(teamId);
team.dropPlayer(player);
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
}
3.TeamEJB
dropPlayer方法会更新TeamEJB-PlayerEJB关系。首先它得到关系字段players 对应的LocalPlayer对象集合,然后调用集合的remove方法删除目标运动员。代码如下:
public void dropPlayer(LocalPlayer player) {
try {
Collection players = getPlayers();
players.remove(player);
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
}
获得一个组里的所有运动员
1.RosterClient
客户段调用RosterEJB的getPlayersOfTeam方法来获得某个组的成员,该方法返回包含PlayerDetails对象的ArrayList。PlayerDetails是PlayerEJB的值对象(《J2EE核心模式》),包含的数据成员playerId、name、salary都是PlayerEJB的持久性字段。客户端调用代码如下:
playerList = myRoster.getPlayersOfTeam("T2");
2.RosterEJB
RosterEJB的getPlayersOfTeam方法先调用TeamEJB的findByPrimaryKey方法找到对应的实例,然后调用TeamEJB的getPlayers方法,最后调用copyPlayerToDetails方法将每一个运动员的数据拷贝到PlayDetails中组成返回集合。代码如下:
public ArrayList getPlayersOfTeam(String teamId) {
Collection players = null;
try {
LocalTeam team = teamHome.findByPrimaryKey(teamId);
players = team.getPlayers();
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
return copyPlayersToDetails(players);
}
copyPlayerToDetails方法的实现如下:
private ArrayList copyPlayersToDetails(Collection players) {
ArrayList detailsList = new ArrayList();
Iterator i = players.iterator();
while (i.hasNext()) {
LocalPlayer player = (LocalPlayer) i.next();
PlayerDetails details =
new PlayerDetails(player.getPlayerId(),
player.getName(), player.getPosition(),
player.getSalary());
detailsList.add(details);
}
return detailsList;
}
3.TeamEJB
TeamEJB的getPlayers方法是关系字段players的访问方法:
public abstract Collection getPlayers();
对本地客户可用,因为它是在Local接口中被定义:
public Collection getPlayers();
该方法向本地客户返回关系字段的引用,如果客户接着修改了得到的结果,实体Bean中的关系字段也跟着被修改(因为是引用,所以操作的是同一个对象)。例如本地客户可以用如下方法开除一个运动员:
LocalTeam team = teamHome.findByPrimaryKey(teamId);
Collection players = team.getPlayers();
players.remove(player);
下面讲到的方法描述如何避免这种危险。
获取组成员的副本
这一小节讨论下面两项技术:
☆ 过滤返回给远程客户端的信息
☆ 防止本地客户直接修改关系字段
1.RosterClient
如果你想在返回给客户端的结果中过滤掉运动员的薪水信息,你应该调用RosterEJB的getPlayersOfTeamCopy方法,该方法跟getPlayersOfTeam的唯一区别就是它把每个运动员的薪水都设置为0。客户端调用代码:
playerList = myRoster.getPlayersOfTeamCopy("T5");
2.RosterEJB
getPlayersOfTeamCopy方法并不像getPlayersOfTeam方法一样调用getPlayers访问方法,它调用LocalTeam接口中定义的getCopyOfPlayers商业方法。从getPlayersOfTeamCopy方法得到的返回值不能修改TeamEJB的关系字段players。代码如下:
public ArrayList getPlayersOfTeamCopy(String teamId) {
ArrayList playersList = null;
try {
LocalTeam team = teamHome.findByPrimaryKey(teamId);
playersList = team.getCopyOfPlayers();
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
return playersList;
}
3.TeamEJB
TeamEJB的getCopyOfPlayers方法返回包含PlayeDetails对象的ArrayList。为了创建这个ArrayList,它必须遍历关系字段players集合并将每一个元素的信息拷贝到一个PlayerDetails对象中,因为要过滤薪水字段,所以只拷贝了salary除外的字段,而salary被直接置为0。当客户端调用getPlayerOfTeamCopy方法时,隐藏了运动员的薪水信息。代码如下:
public ArrayList getCopyOfPlayers() {
ArrayList playerList = new ArrayList();
Collection players = getPlayers();
Iterator i = players.iterator();
while (i.hasNext()) {
LocalPlayer player = (LocalPlayer) i.next();
PlayerDetails details =
new PlayerDetails(player.getPlayerId(),
player.getName(), player.getPosition(), 0.00);
playerList.add(details);
}
return playerList;
}
根据位置查询运动员
1.RosterClient
客户端调用RosterEJB的getPlayersByPosition方法:
playerList = myRoster.getPlayersByPosition("defender");
2.RosterEJB
RosterEJB的getPlayersByPosition方法调用PlayerEJB的findByPosition方法得到特定位置得运动员集合:
public ArrayList getPlayersByPosition(String position) {
Collection players = null;
try {
players = playerHome.findByPosition(position);
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
return copyPlayersToDetails(players);
}
3.PlayerEJB
LocalPlayerHome接口定义了findByPosition方法:
public Collection findByPosition(String position)
throws FinderException;
因为PlayerEJB是CMP实现的实体Bean,所以实体Bean类PlayerBean并不实现查找方法,而是在部署描述符中为每一个查找方法指定EJB QL查询。下面是findByPosition方法的EJB QL查询:
SELECT DISTINCT OBJECT(p) FROM Player p
WHERE p.position = ?1
Deploytool工具会将上面的EJB QL查询转换成对应的SELECT语句。在容器调用findByPositiong方法的时候,SELECT语句也将被执行。
查询运动员的运动项目
1.RosterClient
客户段调用RosterEJB的getSportsOfPlayer方法:
sportList = myRoster.getSportsOfPlayer("P28");
2.RosterEJB
RosterEJB的getSportsOfPlayer方法返回一个运动员可以参与的运动项目的String类型的ArrayList,它直接调用PlayerEJB的getSports方法得到这个ArrayList。代码如下:
public ArrayList getSportsOfPlayer(String playerId) {
ArrayList sportsList = new ArrayList();
Collection sports = null;
try {
LocalPlayer player =
playerHome.findByPrimaryKey(playerId);
sports = player.getSports();
} catch (Exception ex) {
throw new EJBException(ex.getMessage());
}
Iterator i = sports.iterator();
while (i.hasNext()) {
String sport = (String) i.next();
sportsList.add(sport);
}
return sportsList;
}
3.PlayerEJB
PlayerEJB的getSports方法只是简单的调用ejbSelectSports方法。因为ejbSelectSports方法的参数是LocalPlayer类型,所以要传递一个实体Bean实例的引用给方法。代码如下:
public Collection getSports() throws FinderException {
LocalPlayer player =
(team.LocalPlayer)context.getEJBLocalObject();
return ejbSelectSports(player);
}
ejbSelectSports方法的代码:
public abstract Collection ejbSelectSports(LocalPlayer player)
throws FinderException;
ejbSelectSports的EJB QL查询语句:
SELECT DISTINCT t.league.sport
FROM Player p, IN (p.teams) AS t
WHERE p = ?1
在部署PlayerEJB前,你要用deploytool来产生对应的SELECT语句。当容器调用ejbSelectSports方法时,也同时执行SELECT语句。
五、运行RosterApp应用程序
启动用到的软件
1. 在命令模式下执行cloudscape -start命令启动Cloudscape数据库服务器。
2. 执行j2ee -verbose命令启动J2EE服务器。
3. 执行deploytool命令运行deploytool部署工具。
部署该应用程序
1. 在deploytool中打开RosterApp.ear(j2eetutorial/examples/ears directory)文件
2. 部署
a) 选定树视图中的RosterApp节点
b) 执行Tools\Deploy菜单命令
c) 在Introduction对话框中,选中Return Client JAR复选框
d) 在Client JAR File域中输入文件名(或者用Browse按钮设置):j2eetutorial/examples/ears/RosterAppClient.jar
e) 一路Next直到Finish
运行客户端
1. 在命令模式下进入j2eetutorial/examples/ears目录
2. 设置环境变量APPCPATH为RosterAppClient.jar所在目录
3. 执行如下命令:
runclient -client RosterApp.ear -name RosterClient -textauth
4. 用户名:guest。密码:guest123。
六、用deploytool工具部署CMP实现的实体Bean
在第2章讲述了打包和部署企业Bean的基本步骤,这一节将介绍deploytool部署CMP实现的实体Bean时的不同之处,图例参考RosterApp配置说明一节。
指定企业Bean类型
在新建企业Bean向导(New\Enterprise Bean菜单)中指定企业Bean类型和持久性管理机制。
1.在EJB JAR对话框中点击Edit按钮打开Edite Contents对话框,添加实体Bean和关联的实体Bean需要的类
2.在General对话框中,选择Bean类型为:Entity
3.在同一个对话框中指定Bean类和用到的接口类(远程或者本地或者两者都有)
4.在Entity Settings对话框中选择持久性机制Container managed persistence(2.0)
选择持久性字段和抽象模式名
可以在上面提到的Entity Settings对话框中设置。这里我们在Entity选项页中设置(在树视图中选中上面新建的实体Bean节点)。见图6-5
1.在Fields To Be Persisted类表中,选中需要存储到数据库中的字段。这些字段名是根据命名规范从定义的访问方法中读出的。
2.指定主键类合主键字段,主键字段唯一标志一个实体Bean实例
3.在Abstract Schema Name域中输入一个抽象模式名,该名字在EJB QL查询中被引用
为查找方法和Select方法定义EJB QL查询
在Finder/Select Mothods对话框中定义EJB QL查询,见图6-6。
1.在Entity页中点击Finder/Select Methods按钮
2.在Method表各种选择要定义EJB QL查询的方法,在EJB-QL域中输入语句
产生SQL、指定表创建机制(Deploy Settings对话框),指定数据库JNDI名、访问用户名和密码(Database Settings对话框),定义关系(EJB JAR节点的Relationship页,图6-3,Edit Relationships对话框,图6-4)都请参考RosterApp配置说明一节的对应内容。
七、CMP的主键
主键类并不一定是J2SE或者J2EE的标准类库中的类(特别是你的主键是组合字段的时候),这是你就要自己新建主键类并把它和实体Bean打包在一起。
主键类
下面的例子中,PurchaseOrderKey类为PurchaseOrderEJB实现一个组合主键,该主键由有两个数据成员productModel和vendorId对应实体Bean的两个持久性字段:
public class PurchaseOrderKey implements java.io.Serializable {
public String productModel;
public String vendorId;
public PurchaseOrderKey() { };
public String getProductModel() {
return productModel;
}
public String getVendorId() {
return vendorId;
}
public boolean equals(Object other) {
if (other instanceof PurchaseOrderKey) {
return (productModel.equals(
((PurchaseOrderKey)other).productModel) &&
vendorId.equals(
((PurchaseOrderKey)other).vendorId));
}
return false;
}
public int hashCode() {
return productModel.concat(vendorId).hashCode();
}
}
对于CMP实体Bean的主键类有以下要求:
☆ 该类必须是public公有类
☆ 数据成员是实体Bean的持久性字段的子集
☆ 有一个public的缺省构造函数(没有实现任何构造函数或者自己实现一个无参构造函数和其他构造函数)
☆ 重载hashCode和equals(Object other)方法(继承自Object类)
☆ 可序列化(实现Serializble接口)
实体Bean类中的主键
在PurchaseBean类中,主键类对应字段(vendorId和productModel)的访问方法定义如下:
public abstract String getVendorId();
public abstract void setVendorId(String id);
public abstract String getProductModel();
public abstract void setProductModel(String name);
下面是PurchaseOrderBean类ejbCreate方法的代码,它的返回类型是主键类但是返回语句缺返回null。虽然不是必需的,null是CMP机制推荐的返回值。这样可以节省资源,因为实体Bean并不需要产生主键类的实例并返回。(This approach saves overhead because the bean does not have to instantiate the primary key class for the return value.这句话有些疑义,它的意思应该是说CMP机制会自动处理这些动作,所以我们不必自己多事浪费时间)。
public PurchaseOrderKey ejbCreate (String vendorId, String productModel, String productName)
throws CreateException {
setVendorId(vendorId);
setProductModel(productModel);
setProductName(productName);
return null;
}
产生主键值
一些实体Bean的主键值对于商业实体有特殊意义。例如:一个表示向呼叫中心打入的电话呼叫的实体Bean,主键应该包括呼叫被接收时的时间戳。也有很多实体Bean的主键值是任意的,只要它们是唯一的。对CMP实现的实体Bean,容器可以自动为实体Bean生成主键值,不过它对实体Bean有一些额外的要求:
☆ 在部署描述符中,定义主键类被为java.lang.Object,不指定主键字段
☆ 在Home接口中,findByPrimaryKey方法的参数是java.lang.Object
☆ 实体Bean类中,ejbCreate的返回值类型是java.lang.Object
这些实体Bean的主键值保存在只有容器可以访问的内部字段中,你不能让它和持久性字段或者任何其他数据成员产生联系,然而你还是可以通过调用getPrimaryKey方法得到主键值,然后你可以调用findByPrimaryKey来定位实体Bean实例了。