Java企业级版本,或者说Java EE(以前叫J2EE),对于开发服务器端的应用来说是一个强大的但却又过于复杂的的平台。从它诞生之日起,过于复杂一直是对使用Java EE犹豫不决的一个重要因素。在JavaWorld的以前的一篇文章”简化之路”中,我指出了那些让Java EE应用变复杂的因素,其中很多都是与当前的EJB 2.1规范有关。
在过去的三年中,Java开放源代码社区,Java社区进程(JCP)以及主要的Java EE供应商,一直致力于让Java EE更简单。举例来说:新的设计范例,比如POJO服务,服务拦截器和依赖注入,已经可以在实际应用中用来简化Java EE的开发了。还有,新的工具和框架,比如Hibernate, AOP(aspect-oriented programming,面向方面编程),Struts,Xdoclet和Spring, 也已经被广泛用于同一目的。
简化不是功能的减少
简化一个编程模型并没有减少它的功能。简化只是把复杂的逻辑隐藏到了框架代码或可重用的组件中去了。根本上,它是把复杂的东西从需要应用开发者直接管理的地方转移到了大多数开发者看不到的地方。
上述的模板和工具让初学者更容易上手,同时也提高了有经验的Java开发者的生产力,现在它们正在被JCP合并到下一代的Java EE标准中(比如:EJB 3.0)。由Java开发人员Raghu Kodali最近所做的研究显示:将Java EE的示例程序RosterApp从EJB 2.1转到EJB 3.0可以减少百分之五十以上的代码。
Java注释是EJB3.0背后的关键,它将POJO服务,POJO持久化和依赖注入一起绑定为一个完整的企业级中间件解决方案。这篇文章中,我使用了一个示例应用:JBoss EJB 3.0 TrailBlazer,来演示使用注释开发轻量级的EJB 3.0 POJO应用。TrailBlazer的应用使用EJB 3.0中不同的工具和API重复实现了一个投资计算器。示例程序完全可以在JBoss 应用服务器4.0.3版本中运行,并且与最新的EJB 3.0标准完全兼容(完成时)。
让我们来开始体验一下注释驱动编程模型的好处吧。
EJB 3.0的注释驱动编程模型
从开发者的观点来看,EJB 3.0广泛地使用了Java 注释.注释有两个关键优势:它们取代了过多的XML配置文件并且消除了严格组件模型需求。
注释 vs XML
基于XML的布署描述和注释一起都可以用来在Java EE应用中配置服务的相关属性。它们的区别在于:XML文档是与代码分开处理的,特别是在运行时刻,而注释是与代码编译在一起的并被编译器检查的。对于开发者来说这就有了一些重要的含义,正如我下面所列出的:
冗长:XML配置文件是出了名的冗长的。为了配置代码,XML文件必须复制许多信息:比如代码中类名字和方法名字。Java注释则不同,它是代码的一部分,不需要额外的引用就可以指明配置信息。
强壮性:在XML文件中重复的代码信息引入了多处出错的可能。比如,如果你写错了方法的名字,那应用直到运行时刻才会出错垮掉。也就是说,XML配置文件的强壮性就不如注释,注释是被编译器检查的,并和其它代码一起被处理的。
灵活性:既然XML文件是在代码之外被单独处理的,那也就是说基于XML的配置信息不是“硬编码”的,是可以以后修改的。部署的灵活性对系统管理员来说是非常非常重要的特性。
注释是简单易用的,已证明对大多数应用来说足够了。XML文件更复杂,但能被用来处理更高级的问题。EJB 3.0允许你通过注释来配置大多数的应用。EJB 3.0也支持用XML文件来覆盖默认的注释,及配置像数据库联接这样的外部资源。
除了替换和简化XML描述符,注释也允许我们废除困扰EJB 1.x, EJB 2.x的严格组件模型。
POJO vs 严格组件
EJB 组件是容器管理(container-managed)的对象。容器在运行时刻操作Bean的状态和行为。为了让行为发生,EJB 2.1规范定义了一个Bean必须遵守的严格的组件模型。每一个EJB类必须从某一种抽象类中继承,并为容器提供了回调的钩子。既然Java只支持单继承,严格组件模型就限制了开发者使用EJB组件创建一个复杂对象结构的能力。当把复杂的应用数据映射到实体 Bean中的时候,正如我们在第二部分中看到的,这会成为一个很大的问题。
在EJB 3.0中,所有的容器服务都可以通过使用注释的POJO应用来配置和交付。大多数情况下,并不需要特殊的组件类。让我们通过JBoss EJB 3.0 TrailBlazer示例看一下如何在EJB 3.0中使用注释。
开发藕合松散的服务对象
像Java EE这样的企业级中间件的一个最重要的好处是允许开发者使用藕合松散的组件来开发应用。这些组件仅仅通过他们自己发布的商业接口来藕合。因此这些组件的实现类可以在不改变应用其余部分的情况下改变自己的实现。这将会使应用更加强壮,更容易测试,更易移植。EJB 3.0使得在POJO中创建藕合松散的商业组件变得更简单了。
Session bean
在EJB 3.0应用中,藕合松散的服务组件的典型应用是Session Bean。一个Session Bean至少有一个接口(也就是:商业接口),其它应用组件通过它获得服务。下面的代码为我们的投资计算器服务提供了商业接口。它只有一个方法,根据给定的起始年龄,终止年龄,增长率,月存金额,计算出总投资额。
public interface Calculator {
public double calculate (int start, int end,
double growthrate, double saving);
}
Session bean类简单地实现了商业接口。你必须通过使用Stateless或Stateful注释来告诉EJB 3.0容器这个POJO类是一个Session Bean。有状态(Stateful)的session bean在不同的服务请求间维护着客户的状态。相反地,对于无状态(Stateless)的session bean,每次的请求都是被随机挑选的session bean实例处理的。这些行为是与EJB 2.1规范中的有状态和无状态session bean的定义是一致的。EJB 3.0容器算出何时实例化Bean对象,并通过商业接口让其可用。下面是session bean实现类的代码:
@Stateless
public class CalculatorBean implements Calculator {
public double calculate (int start, int end,
double growthrate, double saving) {
double tmp = Math.pow(1. + growthrate / 12.,
12. * (end - start) + 1);
return saving * 12. * (tmp - 1) / growthrate;
}
}
你也可以为一个session bean指明多个接口-一个为本地客户服务,一个为远程客户服务。只要使用@Local和@Remote注释来区分。下面的代码片断显示了同时实现了本地和远程接口的CalculatorBean。如果你没有@Local和@Remote注释,session bean接口默认为本地接口。
@Stateless
@Local ({Calculator.class})
@Remote ({RemoteCalculator.class})
public class CalculatorBean implements Calculator, RemoteCalculator {
public double calculate (int start, int end,
double growthrate, double saving) {
double tmp = Math.pow(1. + growthrate / 12., 12. * (end - start) + 1);
return saving * 12. * (tmp - 1) / growthrate;
}
public String getServerInfo () {
return "This is the JBoss EJB 3.0 TrailBlazer";
}
}
Session bean用户通过JNDI得到bean的一个存根(Stub)对象。容器所提供的存根对象实现了session bean的商业接口。所有针对存根的调用都被引向了容器,由容器调用相应的实现类中的接口。对于有状态的的session bean,你必须自己在客户端缓存存根对象,这样在每次的后续调用时,容器才知道要提供相同的的bean实例。下面的片断显示如何调用session bean.在后面,你将会学到获取存根对象的更简单的方法。
InitialContext ctx = new InitialContext();
cal = (Calculator) ctx.lookup(Calculator.class.getName());
double res = cal.calculate(start, end, growthrate, saving);
Session bean生命周期的管理
为达到藕合松散的目的,应用把session bean实例的创建、缓存、销毁全部交给EJB 3.0容器(也就是,反向控制设计模式)。应用只和bean的商业接口打交道。
但如果应用需要对session对象更好的控制呢?比如说,应用可能需要在创建session bean的时候初始化数据库联接,而在销毁bean时关闭外部的联接。上述这些,你都可能通过在bean类中定义生命周期的回调方法来实现。这些方法将会被容器在生命周期的不同阶段调用(如:创建或销毁时)。通过使有下面所列的注释,EJB 3.0允许你将任何方法指定为回调方法。这不同于EJB 2.1,EJB 2.1中,所有的回调方法必须实现,即使这是空的。EJB 3.0中,bean可以有任意数量,任意名字的回调方法。
@PostConstruct:当bean对象完成实例化后,使用了这个注释的方法会被立即调用。这个注释同时适用于有状态和无状态的session bean。
@PreDestroy:使用这个注释的方法会在容器从它的对象池中销毁一个无用的或者过期的bean实例这前调用。同时适用于有状态和无状态的session bean.
@PrePassivate:当一个有状态的session bean实例空闲过长的时间,容器将会钝化它,并把它的状态保存下来。使用这个注释的方法会在容器钝化bean实例之前调用。适用于有状态session bean。
@PostActivate:当客户端再次使用已经被钝化的的有状态session bean时,新的实例被创建,状态被恢复。使用此注释的session bean会在bean的激活完成时调用。
@Init:这个注释指定了有状态session bean初始化的方法。它区别于@PostConstruct注释在于:多个@Init注释方法可以同时存在于有状态session bean 中,但每个bean实例只会有一个@Init注释的方法会被调用。这取决于bean是如何创建的(细节请看EJB 3.0规范)。@PostConstruct在@Init之后被调用。
另一个有用的生命周期方法注释是@Remove,特别是对于有状态session bean。当应用通过存根对象调用使用了@Remove注释的方法时,容器就知道在该方法执行完毕后,要把bean实例从对象池中移走。
@Stateful
public class CalculatorBean implements Calculator, Serializable {
// ... ...
@PostConstruct
public void initialize () {
// Initializes the history records and load
// necessary data from database etc.
// 初始化历史记录,并从数据库中装入必需的数据。
}
@PreDestroy
public void exit () {
// Save history records into database if necessary.
// 如有必要则将历史记录保存至数据库中
}
@Remove
public void stopSession () {
// Call to this method signals the container
// to remove this bean instance and terminates
// the session. The method body can be empty.
// 调用这个方法来通知容器将bean实例移除并中止session.
// 这个方法可以为空。
}
// ... ...
}
消息驱动bean
Session bean服务提供了同步调用的方法。另一个重要的藕合松散服务类型是一种通过进入的消息来触发的异步服务(比如:email或Java消息服务产生的消息)。EJB 3.0的消息驱动bean(MDB)是设计用来专门处理基于消息请求的组件。
一个MDB类必须实现MessageListener接口。当容器检测到bean守候的队列一条消息时,就调用onMessage()方法,将消息作为参数传入。MDB在OnMessage()中决定如何处理该消息。你可以用注释来配置MDB侦听哪一条队列。当MDB部署时,容器将会用到其中的注释信息。在下面的例子中,CalculatorBean MDB会在JMS队列queue/mdb有消息进入时调用。MDB解析消息,并根据消息内容计算投资。
@MessageDriven(activateConfig =
{
@ActivationConfigProperty(propertyName="destinationType",
propertyValue="javax.jms.Queue"),
@ActivationConfigProperty(propertyName="destination",
propertyValue="queue/mdb")
})
public class CalculatorBean implements MessageListener {
public void onMessage (Message msg) {
try {
TextMessage tmsg = (TextMessage) msg;
Timestamp sent =
new Timestamp(tmsg.getLongProperty("sent"));
StringTokenizer st =
new StringTokenizer(tmsg.getText(), ",");
int start = Integer.parseInt(st.nextToken());
int end = Integer.parseInt(st.nextToken());
double growthrate = Double.parseDouble(st.nextToken());
double saving = Double.parseDouble(st.nextToken());
double result =
calculate (start, end, growthrate, saving);
RecordManager.addRecord (sent, result);
} catch (Exception e) {
e.printStackTrace ();
}
}
// ... ...
}
依赖注入
在上一节中,你学到了如何开发藕合松散的服务组件。但是,为了存取那些服务对象,你需要通过服务器的JNDI来查找存根对象(session bean)或消息队列(MDB)。JNDI查找是把客户端与实际的服务端实现解藕的关键步骤。但是,直接使用一个字符串来进行JNDI查找并不优雅。有这样几个原因:
客户端与服务端必须有一致的基于字符串的名字。它没有在编译时得到认证或在布署时得到检查。
从JNDI返回的服务对象的类型没有在编译时进行检查,有可能在运行时出现转换(casting)错误。
冗长的查找代码,有着自己的try-catch代码块,在应用之间是重复的和杂乱的
EJB 3.0,对任何POJO,提供了一个简单的和优雅的方法来解藕服务对象和资源。使用@EJB注释,你可以将EJB存根对象注入到任何EJB 3.0容器管理的POJO中。如果注释用在一个属性变量上,容器将会在它被第一次访问之前赋值给它正确的值。下面的例了演示了怎样把CalculatorBean无状态session bean的存根注入到CalculatorMDB MDB类中。
public class CalculatorMDB implements MessageListener {
@EJB Calculator cal;
// Use the cal variable
// ... ...
}
注释如果被用在JavaBean风格的setter方法上时,容器会在属性第一次使用之前,自动地用正确的参数调用bean的setter方法。下面的片断演示了这是如何做的:
public class CalculatorMDB implements MessageListener {
Calculator cal;
@EJB
public void setCal (Calculator cal) {
this.cal = cal;
}
// Use the cal variable
// 使用cal变量
// ... ...
}
除@EJB注释之外,EJB 3.0也支持@Resource注释来注入来自JNDI的任何资源。下面的例子中,我演示了如何注入服务器端默入的TimerService和SessionContext对象,也演示了如何注入来自JNDI的命名数据库和JMS资源。
@Resource
TimerService tms;
@Resource
SessionContext ctx;
@Resource (name="DefaultDS")
DataSource myDb;
@Resource (name="ConnectionFactory")
QueueConnectionFactory factory;
@Resource (name="queue/A")
Queue queue;
此外,你也可以把一个容器管理的持久化管理器(也就是,EntityManager-类似于Hibernate session对象)注入到EJB 3.0 POJO中。
把容器服务交给POJO
除了管理生命周期和访问藕合松散的服务对象外,EJB 3.0通过简单的注释也为POJO提供了运行时刻服务。
事务
最有用的容器服务可能就是事务管理服务,当应用出现失败或异常时,它保证了数据库的完整性。你可以简单地将为一个POJO方法申明它的事务属性。这样容器就可以在合适的上下文中运行这个方法。举例来说,下面的代码申明了容器在运行updateExchangeRate()时必须创建一个新的事务。当这个方法退出时提交事务。实际上,所有在updateExchangeRate()中被调用的方法都在此事务中运行,除非有特别申明。在updateExchangeRate()中的数据库操作要么全部成功,要么全部失败。
@Stateless
public class CalculatorBean implements Calculator {
// ... ...
@TransactionAttribute(TransactionAttributeType.REQUIRED)
public void updateExchangeRate (double newrate) throws Exception {
// Update the database in a loop.
// 在循环中更新数据库
// ... ...
// The operations in the loop must all be successful or
// the database is not updated at all.
// 循环中的操作必须全部成功或者根本不更新。
}
}
安全
容器也提供了安全服务来进行用户认证和根据用户规则来限制对POJO的访问。对每一个POJO来说,你可以通过使用@SecurityDomain注释为它指定一个安全域, 安全域告诉容器到哪里去找密码和用户角色列表。JBoss中的other域表明文件是classpath中的users.propertes和roles.properties。这样,对每一个方法来说,我们可以使用一个安全限制注释来指定谁可以运行这个方法。比如,下面的例子,容器对所有试图调用addFund()的用户进行认证,只允许拥有AdminUser角色的用户实际运行它。如果你没有登录或者没有以管理员的身份登录,一个安全意外将会抛出。
@Stateless
@SecurityDomain("other")
public class CalculatorBean implements Calculator {
@RolesAllowed({"AdminUser"})
public void addFund (String name, double growthrate) {
// ... ...
}
@RolesAllowed({"AdminUser"})
public void addInvestor (String name, int start, int end) {
// ... ...
}
@PermitAll
public Collection <Fund> getFunds () {
// ... ...
}
// ... ...
@RolesAllowed({"RegularUser"})
public double calculate (int fundId, int investorId,
double saving) {
// ... ...
}
}
通用拦截器
事务和安全服务都可以被看作是容器管理的运行时刻拦截器。容器拦截了对EJB存根的调用,并在其上应用事务上下文或进行安全限制。
在EJB 3.0中,你可以自己写拦截器来扩展容器服务。使用@AroundInvoke注释,你可以将任意bean方法作为拦截器方法在任意bean方法之前和之后运行。下面的例子中,log()方法是一个拦截器,它计算和记录了其它bean方法的执行时间:
@Stateful
public class CalculatorBean implements Calculator {
// Bean methods that are to be intercepted by "log()"
// bean方法将被log()方法拦截
//
// ... ...
@AroundInvoke
public Object log (InvocationContext ctx)
throws Exception {
String className = ctx.getBean().getClass().getName();
String methodName = ctx.getMethod().getName();
String target = className + "." + methodName + "()";
long start = System.currentTimeMillis();
System.out.println ("Invoking " + target);
try {
return ctx.proceed();
} catch(Exception e) {
throw e;
} finally {
System.out.println("Exiting " + target);
cal.setTrace(cal.getTrace() + "
" +
"Exiting " + target);
long time = System.currentTimeMillis() - start;
System.out.println("This method takes " +
time + "ms to execute");
}
}
}
下一步?
在第一部分中,我大致地讨论了EJB 3.0基于POJO的编程模型和如何在EJB 3.0中开发藕合松散的服务组件。在第二部分中,我会讨论EJB 3.0的另一个主要的概念:可管理的POJO持久性。
使用EJB 3.0简化Java开发(二)
在第一部分中,我讨论了在企业级JavaBean 3.0(EJB)中注释驱动的POJO编程模型。我阐述了如何开发POJO服务,如何让容器服务使用POJO, 如何使用依赖注入来组合应用。这些POJO服务主要是用来封装应用的商业逻辑。在商业逻辑的背后,现今的大多数应用都有由一个高性能的关系型数据库作为支撑的数据模型层。
在第二部分中,我将讨论EJB 3.0实体bean如何利用POJO和注释的优势来极大地简化你的数据模型以及它们与后台关系数据库的持久化。在我们进入EJB 3.0实体bean的细节之前,让我们先来看一下为什么对于企业级Java应用,数据模型和持久化是如此巨大的一个挑战。
对象-关系映射(ORM)
在Java虚拟机中,所有的数据都被模型化并且封装在了类和对象的树结构中。但是在后端的关系型数据库中,数据被模型化为关系型表,它们通过共享的键域(外键)相互关联起来。相同的数据却有两个视图,这对企业级Java的开发者来说是一个艰难的挑战:当你想从持久化的数据存储中存取数据时,你必须在对象与关系表达之间来回转换,这一过程叫做对象-关系映射(ORM)。在Java EE(Java企业版,以前叫做J2EE),你可以通过两个途径来实现对象-关系映射。
手动的:你可以使用Java数据库连接来直接操作持久化-对于简单应用的直截了当的解决方案。JDBC API的类是紧贴在关系型数据库表、行和列之后的数据模型。你必须手动地在应用的内部对象模型与JDBC对象模型之间进行转换,如果你的应用的内部模型本身就类似于2维的关系表的话,那采用JDBC是最佳手段。
自动的:你可以把ORM交给框架。框架通常向你提供一个可以和任意数据对象进行交互的API。通过那个API,你可以存储、获取和查询数据库。框架在后台完成了框架对象的转换。因为特定的关系型SQL查询不适合对象接口,ORM框架通常定义它自己的查询语言,并且自动为当前关系型数据库生成正确的SQL语句。对于拥有复杂的数据模型的应用来说,基于框架的手段能为你节省很多时间并降低了出错的可能。
对象数据库
一个对象型数据库直接在数据库中存储、获取和查找对象。因为不再需要ORM,所以它对于Java应用非常适合。不幸的是,现今的对象型数据库相对于关系型数据库来说还不成熟,速度也慢。你可以这样说,一个好的ORM框架从根本上来说,就是为关系型数据库提供一个对象型数据库的接口。两者它都要做到最好。
这篇文章,我将重点放在专为企业级Java ORM应用设计的自动框架上。下一节,我将提到几个流行的ORM框架和EJB 3.0中几个关键的革新。
ORM 框架
EJB 实体bean是Java EE中“官方”的ORM解决方案。但是,在EJB1.x和2.x中,实体bean的难以使用是出了名的,原因如下:
●EJB 1.x和2.x实体bean必须遵守一种严格的组件模型。每一个bean类必须实现一个home接口和一个商业接口。它们必须从某种抽象类中继承,而且必须实现其所有方法,即使它们多数为空。这样的一种严格组件模型使得想从EJB 1.x和2.x的实体bean中构建面向对象的数据模型几乎变得不可能了。
●EJB 1.x和2.x容器需要特别冗长的xml配置文件来建立实体bean与关系型数据库中的表映射。那些文件是非常单调乏味和容易出错的。
简而言之,EJB 1.x和2.x实体bean是一个设计拙劣的ORM框架。它既没有满足Java数据对象模型的需求,也没有满足关系表数据模型的需求。出于对EJB 1.x和2.x实体bean的不满,开发者开始寻找其它的ORM方案。实际使用中,开源的Hibernate(JBoss开发)和Oracle公司的TopLink是最成功的两个POJO ORM框架。Hibernate和TopLink都是基于POJO的。它们不依赖于任何预定义的组件模型。作为替代,它们使用POJO数据对象(简单的JavaBean式的),自动地解读出如何映射它们,以及它们之间的关系(关系型数据库)。通常,JavaBean类映射到一张数据库表,并根据数据库表中的外键映射出类之间的关系。你可以在一个简单直接的xml配置文件中指明ORM的配置信息,比如JavaBean类对应的表名和属性对应的列名。你可以通过框架中的工具(如:Hibernate中的Session类)来对那些POJO进行操作(如:存储、获取和查找)。
EJB 3.0是建立在Hibernate和TopLink的思想和成功之上。它为Java EE提供了一个标准的POJO ORM框架。另外,EJB 3.0有两个超越现今所有持久化解决方案的关键革新:
●没有使用XML文件来指明ORM配置信息, EJB 3.0允许开发者直接在POJO代码中注释出映射信息。举例来说,你可以用注释来指明每个JavaBean属性对应的关系型表列。在这篇文章的后面,你将看到更多的例子。注释使得映射更直接,更容易维护了。
●EJB 3.0为实体bean定义了一个新的归档格式。每个档案使用一组独立的,为后端数据库和ORM行为所专用的配置集来定义一个持久化上下文。在这篇文章的后面,我会讨论持久化上下文。
现在,让我们通过几个简单的例子来看一下EJB 3.0是如何完成POJO ORM的。
映射一个简单的对象
在EJB 3.0中,每个实体bean都是一个简单的JavaBean式的类。为了告诉EJB 3.0容器这个类应该为持久化进行映射,你应该用@Entity来注释这个类。
每一个实体bean类映射到一个关系型数据库表。默认地,表名对应类名。你可以用@Table来为类指定另一个表。每一个JavaBean属性映射到表的列上,同样的,默认列名就是属性名。你可以用@Column注释在属性的Setter方法上来改变这种默认关系。下面是一个EJB 3.0的简单例子:
@Entity
// @Table (name="AlternativeTableName")
public class Person implements Serializable {
protected int id;
protected String name;
protected Date dateOfBirth;
public void setId (int id) {
this.id = id;
}
@Id(generate = GeneratorType.AUTO)
public int getId () {
return id;
}
public void setName (String name) {
this.name = name;
}
// @Column (name="AlternativeColumnName")
public String getName () {
return name;
}
public void setDateOfBirth (Date dateOfBirth) {
this.dateOfBirth = dateOfBirth;
}
public Date getDateOfBirth () {
return dateOfBirth;
}
}
当容器把Person类映射到Person SQL数据库表以后,每一个Person实例就是表中的一条数据记录。
映射一个简单的JavaBean类是容易的。但自动ORM框架真正闪光之处在于映射互相关联的对象。下一节中,我们看一下EJB 3.0如何操作对象间的关系。
关系
在一个数据模型里面,一般来说类相互之间都会有某种联系。比如,一个Person(个人)对象可以和一个Resume(确认)对象相关联,反过来也一样(一对一关系);一个Person对象可以和多个CreditCard(信用卡)对象相关,而一个CreditCard对象只能和一个Person对象相关(一对多关系)。多个Person对象可以和一个Address(地址)对象相关,而一个Person对象只能对应一个Address对象(多对一关系)。(译者注:此处原著笔误, Person与Address位置颠倒了;编者注:我看两者是多对多的关系。一家人住在同一个地方,这个地址对于这一家人来说是一对多的关系;房主在别的地方又买了一套房,房主与地址的关系是一对多的关系。)
在一个数据模型中,对象指针用来操作那些关系。举例来说,一个Person对象可以有一个属性(也就是域)指向一个Resume对象。而另一个属性是CreditCard对象的集合。为了告知EJB 3.0容器对象间的关系,你只需简单地在POJO中注释JavaBean属性。
@Entity
public class Person implements Serializable {
// ... ...
protected Resume resume;
protected CreditCard [] cards;
protected Address addr;
// ... ...
@OneToOne
public Resume getResume () {
return resume;
}
// ... ...
@ManyToOne
// @JoinColumn (name="MyCustomId")
public Address getAddr () {
return addr;
}
// ... ...
@OneToMany
public Collection <CreditCard> getCards () {
return cards;
}
}
在关系型数据库中,那些关系自动地被EJB 3.0容器使用外键来重建了。举例来说,Person表有一个外键包含了Resume表中相应的主键。运行时,EJB 3.0容器加强了一对一的关系:它保证了Resume键值对于Person表中的每一行是唯一的。为了启用Resume表到Person表的双向查询,你可以Resume表中定义一个Person属性,并把它也加上@OneToOne注释。
Person表中也有一个外键包含了Address表中相应行的主键。这种情况下,相同的Address主键可以出现在多个Person行中,这是多对一关系。对于一对多的关系,映射稍有一点复杂,因为外键列是定义在多对一表中的。于是,在CreditCard类中,你必须用@ManyToOne来定义一个Person属性。
改变外部键字段名
ORM中使用的外部键字段的名字是由容器自动决定的或者由@JoinColumn注释来显式的指定。
上面讨论的关系只是实体bean之间关系的一种类型,实体类之间另外一种重要关系是继承。
继承
面向对象设计方法的一个关键概念是继承。使用继承,你可以创建一个复杂的对象树而不需要重复的代码。举例来说,Consultant(顾问)是提供有偿服务的一个人,那么在我们的数据模型中,Consultant类就从Person(个人)类中继承,并增加了一个价格属性。不幸的是,当今的关系型数据库并不存在继承的概念。ORM框架主要通过以下两个手段来模仿这种行为:
●框架可以为每一个类生成一个单独的表。子类的表重复了那些从父类的字段。子类和父类都存储为各自对应的表。
●框架可以使用包含了所有子类属性的表。两种类(父类和子类)的实例都存储于同一张表—父类中不存在的字段(也就是,子类的字段)取null值。为了使继承映射更为强壮,表也可以有一个“区别”列,它存储的标记表明该行数据映射到哪一个类。
EJB 3.0实体bean支持上述两种映射策略,默认是单表映射策略。你可以简单地用注释指明子类的继承策略和区别字段的名字。下面是Consultant类的例子,它从Person类中继承:
@Entity
@Inheritance(discriminatorValue="C")
@DiscriminatorColumn(name="person_type")
public class Consultant extends Person {
protected double rate;
public void setRate (double rate) {
this.rate = rate;
}
public double getRate () {
return rate;
}
}
从上面的例子中,容器使用默认策略将Consultant类映射到Person类对应的同一张表中。如果表中的person_type字段的值为C,那么那一行数据就代表了一个顾问类。否则,当前行代表的是一个普通的Person类。
持久化档案
现在你的数据模型有了一组使用了注释EJB 3.0实体bean的类,你可以将它们捆绑在一起布署到服务器环境中。EJB 3.0为实体bean定义了一种特殊的归档格式,叫做持久化档案(文件后缀名为.par)。
一个.par文件是一组实体bean类文件加上一个简单的配置文件META-INF/persistence.xml的jar打包文件。persistence.xml文件定义了持久化上下文,它告知EJB 3.0哪一个后端数据库(数据源)与这一组实体bean相对应。persistence.xml也包含了配置属性的细节。举例来说,JBoss EJB 3.0是在Hibernate 3.0之上实现的,于是你可以通过persistence.xml传递任意的Hibernate配置选项。这有一个范例persistence.xml文件,它包含了JBoss和Hibernate专用的配置属性,包括SQL 方言(dialect)和二级缓存。
<entity-manager>
<name>cal</name>
<jta-data-source>java:/DefaultDS</jta-data-source>
<properties>
<property name="hibernate.dialect"
value="org.hibernate.dialect.MySQLDialect" />
<property name="hibernate.cache.provider_class"
value="org.jboss.ejb3.entity.TreeCacheProviderHook"/>
<property name="hibernate.treecache.mbean.object_name"
value="jboss.cache:service=EJB3EntityTreeCache"/>
</properties>
</entity-manager>
实体管理器
一旦你部署了实体bean, 你必须通过EJB 3.0的实体管理器的API来访问和操作它们。EJB 3.0容器为每个部署的持久化上下文(也就是,.par文件)提供了一个实体管理器对象。从一个EJB 3.0 session bean POJO(参看第一部分)中 ,你可以通过@PersistenceContext注释将实体管理器对象注入,并传入上下文的名字。
@Stateless
public class ManagerBean implements Manager {
@PersistenceContext (unitName="cal")
protected EntityManager em;
// Use "em"
// 使用“em”
// ... ...
}
基本操作
要创建一个数据对象并把它存入数据库中,你只需简单地使用Java的new关键字来创建POJO,并把它传给EntityManager.persist()方法。
Person p = new Person ();
p.setName ("A new baby");
p.setDateOfBirth (new Date ());
em.persist (p);
要从数据库中取得对象,你可以使用EJB 3.0查询语言来搜索数据库。下面的例子演示了如何将Person表中的所有行作为Person Java对象的集合来返回。
// 得到所有人的对象
Collection <Person> persons = (Collection <Person>)
em.createQuery("from Person p").getResultList();
可管理的POJO
通过实体管理器保存和获取的对象是被管理在持久化上下文中的。这意味着如果对象后来被改变了,那这种改变将会被自动检测并持久化到数据库中。在下面的例子中,我们更新了一个可管理的POJO的一个属性。这个改变会被EJB 3.0容 器自动检测到并发送给了数据。
Person p = em.find(Person.class, personId);
p.setName ("Another Name");
//p会在当前事务结束时被自动地更新到数据库中去。
// 并没用更多的API调用
既然EJB 3.0实体仅只是POJO,那么它们就可以能够被序列化并通过网络传递。如果一个对象不是被容器创建的(也就是说,它是从网络连接中传递过来的或者是某一个远程调用返回的结果),那么持久化上下文并不会管理它。不过,你可以通过调用EntityManager.merge()方法将一个非管理的POJO合并到持久化上下文中。下面是将一个解序列化的POJO合并入当前持久化上下文中的例子。
InputStream in;
// 初始化输入流
Person p = Util.deserialize (in);
// ... ...
em.merge (p);
// p现在是一个可管理的对象了。p的任何改变将会被自动检测并持久化
p.setName ("Another Name");
数据库同步
当实体管理器对象在一个session bean中使用时,它是和服务器的事务上下文绑定的。实体管理器在服务器的事务提交时提交并且同步它的内容。在一个session bean中,服务器的事务默认地会在调用堆栈的最后提交。当然,你也可以通过注释来为每个商务方法指定具体的事务属性。下面的例子展示了如何为一个session bean的方法声明一个新的事务。
@TransactionAttribute(TransactionAttributeType.REQUIRESNEW)
public void update () {
// 这个方法更新Person对象
// 更新将会在这个方法的末尾被提交和刷新到数据库中
批处理中刷新数据库操作
为了只在当事务提交时才将改变更新到数据库中,容器将所有数据库操作集中到一个批处理中,这样就减少了代价昂贵的与数据库的交互。
如果你需要在事务提交之前将更新刷新到数据库中,你可以直接地调用EntityManager.flush()方法。或者你可以将一个方法注释为@FlushMode(FlushModeType.NEVER),于是事务管理器将不会在方法的结尾(也就是事务的结尾)处刷新更新到数据库中。这种情况下,你可以手工地来刷新数据库以获得对数据库操作的最大控制。
总结
EJB 3.0 提供了一种简单有效的框架将Java POJO映射到SQL数据库中的关系型表中。它基于Java类中的名字和结构进行智能的默认映射策略。但你也可以用一组简单的注释重载所有的默认值,来处理复杂的对象关系。
EJB 3.0实体管理器提供了简单的API来持久化和从数据库中查找对象。每一个实体管理器对象与一组映射的POJO相关联,并有着它自己的数据库设置。它会自动地捆绑到服务器的事务管理器中。