第9章 Seam 与 对象关系映射
Seam对两种流行的Java持久化体系结构提供广泛支持:Hibernate3,和EJB 3.0引入的Java持久化API。Seam独特的状态管理体系结构允许任何网页应用程序框架的最典型的ORM(对象关系映射)集成。
9.1. 介绍
Seam 是由Java应用程序体系结构的前一代的无状态性典型Hibernate项目的挫败而来。Seam状态管理体系结构最初是设计来解决与持久化相关的问题——与乐观事务处理相关的特殊问题。在线可伸缩应用程序总是使用乐观事务。一个原子(数据库/JTA(Java Transaction API))级事务不应该横越一个用户交互,除非被设计来仅支持很少量的并发客户端。但是,几乎所有趣的工作都首先涉及显示数据给一个用户,然后,稍后,更新同样的数据。所以,Hibernate 被设计来支持一个横越一个乐观事务的持久化上下文的想法。
不幸的是,先于Seam 和EJB 3.0所谓“无状态”体系结构,没有结构来表示一个乐观事务。因而,作为代替,这些体系结构对原子事务提供范围持久化上下文。当然,这导致更多的问题给用户,并且引起用户抱怨Hibernate:恐惧的LazyInitializationException(延迟初始化异常)。我们需要的是有一个结构在应用程序层来表示一个乐观事务。
EJB 3.0认识到这个问题,并且提出了有状态组件的想法(一个有状态会话bean),对组件的存在期具有扩展的范围持久化上下文。这是一个对这个问题的局部解决方案(和是一个进/出它自己的有用的结构),然而,有两个问题:
* 有状态会话bean的生命周期必须通过网页层手动管理(与它宣称的相比,在实践中它产生了一系列问题和更多的困难)
* 在同一个乐观事务中的有状态组件之间传播持久化上下文是可能的,然而,需要技巧。
Seam通过提供对话解决了第一个问题,并且有状态会话bean组件延伸到对话(实际上,大部分对话表示在数据层的乐观事务)。对多数简单的应用程序这个足够的(如Seam booking demo例子),在那儿不需要传播持久化上下文。对更复杂应用程序,在每一个对话中带有许多松散交互组件,越过组件传播持久化上下文成为一个重要问题。所以,Seam扩展了EJB 3.0的持久化管理模型,提供范围对话扩展了持久化上下文。
9.2. Seam管理事务
EJB会话beans展示了公布的事务管理。当bean被调用时,EJB容器能透明地启动一个事务,并且当调用结束时结束它。如果我们写了一个会话bean方法,作为一个JSF动作接收器,我们能做与在一个事务中的动作相关联的所有工作,并且确保它被提交或者当我们完成处理动作时回滚。这是一个好特色,并且相当多的Seam应用程序需要所有这一切。然而,用这个方法有一个总题,一个Seam应用程序可能不执行对从单个方法调用到一个会话bean的一个请求的全部数据访问。
* 请求可能需要通过几个松耦合组件处理,它们每一个从网页层独立地被调用。了解几个或者甚至了解从网页层到在Seam中的EJB组件的每请求的大部分调用是普通的。
* 视窗的渲染可能要求延迟联合接收。
事务每请求越多,当我们的应用程序处理多数并发请求时,我们遭遇原子态和隔离问题就越可能。 当然,所有写的操作应该发生在同一个事务中!
Hibernate用户围绕这个问题开发了“在视窗中打开会话”模式。在Hibernate社区,“在视窗中打开会话”在历史上是十分重要,因为象Spring这类的框架使用了范围事务持久化上下文。所以,当非接收联合被访问时,渲染视窗将引发LazyInitializationExceptions。
这个模式常被作为横越整个请求的单个事务来实现。使用这种模式有几个问题,最严重的是在我们提交之前,我们不能确保事务是成功的——除非是在“在视窗中打开会话”时事务被提交,视窗被完全渲染,并且渲染响应可能已经冲洗到客户端了。我们怎么样才能通知用户他们的事务没有成功呢?
Seam 解决了事务的隔离问题和联合接收问题,虽然围绕这个问题使用了“在视窗中打开会话”。解决方案分成了两部分:
*使用了一个延伸到对话的扩展持久化上下文,代替了事务。
* 使用了两个事务每请求;第一个横越恢复视窗阶段开始(在申请请求值阶段开始稍后,一些事务管理者开始事务)直到调用应用程序阶段结束;第二个横越渲染响应阶段。
在下一章节,我们将告诉你如何设置一个范围对话持久化上下文。但是,首先我们需要告诉你怎么能使Seam事务管理运作。注意,不使用Seam事务管理,你也能使用范围对话持久化上下文,并且,即使当你不使用Seam管理持久化上下文时,也存在有使用Seam事务管理的好原因。然而,两个工具被设计来一起工作,并且当一起使用时,能工作得很好。
即使你使用EJB 3.0容器管理持久化上下文,Seam事务管理也是很有用的。除此之外,如果你在Java EE 5环境外使用Seam,或者在其它情况下,你想使用一个Seam管理持久化上下文,它是特别有用。
9.2.1. 禁止Seam管理事务
对所有JSF请求,在缺省时,Seam事务管理是激活的。如果你想让禁止它,你能在components.xml做它:
<core:init transaction-management-enabled="false"/>
<transaction:no-transaction />
9.2.2. 配置一个Seam 事务管理器
Seam就一个事务的开始、提交、回滚和同步提供了一个事务管理抽象。缺省时,Seam使用一个JTA事务组件,其集成了容器管理和可编程EJB事务。如果你工作在一个Java EE 5环境,你需要在components.xml安装EJB同步组件:
<transaction:ejb-transaction />
然而,你工作在一个非EE 5容器,Seam会试着自动侦听使用的事务同步机制。可是,如果Seam不能侦听出使用的合适的同步,你可能发现你需要象下面这样的一个配置:
* JPA RESOURCE_LOCAL事务带着 javax.persistence.EntityTransaction接口。EntityTransaction 开始事务在申请请求值阶段的开始。
* Hibernate管理事务带着org.hibernate.Transaction接口。HibernateTransaction开始事务在申请请求值阶段的开始。
* Spring管理事务带着org.springframework.transaction.PlatformTransactionManager接口。Spring的PlatformTransactionManagement 管理器可能开始事务在申请请求值阶段,如果userConversationContext属性被设置。
* 明确禁止Seam managed transactions。
配置JPA RESOURCE_LOCAL事务管理,通过增加下列各项在你的components.xml中,在#{em}的地方是持久化: 管理持久化上下文组件的名字。如果你的管理持久化上下文命名为entityManager,你能够省去entity-manager的属性(请看Seam管理持久化上下文)
<transaction:entity-transaction entity-manager="#{em}"/>
配置Hibernate管理事务,声明下列各项在你的components.xml中,在#{hibernateSession}的地方是项目的持久化:管理hibernate会话组件的名字。如果你的管理hibernate会话命名为session,你能够省去session的属性(请看Seam管理持久化上下文)
<transaction:hibernate-transaction session="#{hibernateSession}"/>
明确禁止Seam管理事务,声明下列各项在你的components.xml中:
<transaction:no-transaction />
对配置Spring管理事务请看使用Spring PlatformTransactionManagement(Spring平台事务管理)。
9.2.3. 事务同步
事务同步对事务相关的事件如beforeCompletion() 和afterCompletion()提供回答信号。在缺省时,Seam使用它自己事务同步组件,其要求明确的Seam事务组件的使用,当提交一个事务时,确保同步回答信号被正确地执行。如果在一个Java EE 5环境,<transaction:ejb-transaction/>组件应该被声明在components.xml中,确保如果容器提交一个事务在Seam的认识之外,Seam同步回答信息被正确地调用。
9.3. Seam受管理持久化上下文
如果你在一个Java EE 5环境外使用Seam,你可能不放心容器为你管理持久化上下文的生命周期。即使在一个Java EE 5环境,你可能有一个复杂的应用程序,其带着许多松耦合组件在单一对话范围内一起合作,并且在这情况下,你可能发现在组件之间传播持久化上下文是需要技巧的和容易出错的。
任何一种情况下,你需要在你的组件中使用一个管理持久化上下文(对JPA)或者一个管理会话(对Hibernate)。一个Seam受管理持久化上下文只是一个内建的Seam组件,管理在对话上下文中的一个EntityManager 或Session实例。你能@In用注入它。
在一个群集环境下Seam管理持久化上下文尤其有效。Seam能执行一个EJB 3.0规范不允许容器使用容器管理扩展持久化上下文的优化。Seam支持扩展持久化上下文的透明故障切换,不需要在节点之间复制任何持久化上下文状态(我们希望在下一个版的EJB规范中修正这个的疏忽)。
9.3.1. 用JAP使用Seam受管理持久化上下文
配置一个管理持久化上下文是容易的。在components.xml中,我们能写:
<persistence:managed-persistence-context name="bookingDatabase"
auto-create="true"
persistence-unit-jndi-name="java:/EntityManagerFactories/bookingData"/>
这个配置创建了一个范围对话Seam组件,名为bookingDatabase,管理用JNDI指定的java:/EntityManagerFactories/bookingData持久化单元的EntityManager实例的生命周期。
当然,你需要确定你已捆绑了EntityManagerFactory到JNDI。在JBoss,你能通过增加下列各项属性设置到persistence.xml。
<property name="jboss.entity.manager.factory.jndi.name"
value="java:/EntityManagerFactories/bookingData"/>
现在我们能注入我们的EntityManager,使用:
@In EntityManager bookingDatabase;
如果你使用EJB3,并标记你的类或方法@TransactionAttribute(REQUIRES_NEW),那么事务和持久化上下文不应该传播到在这个对象上的方法调用。然而,因为Seam受管理持久化上下文传播到在对话内的任何组件,所以它会传播到标记为REQUIRES_NEW的方法。因此,如果你标记一个方法REQUIRES_NEW,那么,你应该使用@PersistenceContext访问实体管理器。
9.3.2. 使用一个Seam受管理 Hibernate会话
Seam受管理 Hibernate会话是相似的。在components.xml:
<persistence:hibernate-session-factory name="hibernateSessionFactory"/>
<persistence:managed-hibernate-session name="bookingDatabase"
auto-create="true"
session-factory-jndi-name="java:/bookingSessionFactory"/>
这里,java:/bookingSessionFactory 是在hibernate.cfg.xml中指定的会话工厂。
<session-factory name="java:/bookingSessionFactory">
<property name="transaction.flush_before_completion">true</property>
<property name="connection.release_mode">after_statement</property>
<property
property>
<property
property>
<property name="connection.datasource">java:/bookingDatasource</property>
...
</session-factory>
注意,Seam不冲洗会话,所以,你在完成前总要用hibernate.transaction.flush,确保在JTA事务提交之前会话被自动冲洗。
我们现在使用代码@In Session bookingDatabase能注入一个受管理Hibernate会话到我们JavaBean组件。
9.3.3. Seam受管理持久化上下文和原子对话
延伸到对话的持久化上下文允许你规划横越达到服务器的多请求的乐观事务,不需要使用merge()操作,不需要在每一请求开始时重载数据,并且不需要纠缠于LazyInitializationException 或 NonUniqueObjectException。
因为使用任何乐观事务管理,通过乐观锁的使用,事务的隔离性和一致性能被达到。幸运地,通过提供@Version注释,Hibernate 和 EJB 3.0都很容易使用乐观锁。
缺省时,持久化上下文在每一个事务结束时被冲洗(同步数据库)。有时候这是想要的行为。然而太多时候,我们更喜欢所有的改变维持在内存,并且只在对话成功结束时写入数据库。对真正的原子对话这是允许的。因为某些非JBoss、非Sun和 非Sybase的 EJB3.0专家组成员的目光短浅 的决定结果,当前没有简单的、可用的和轻便的方法使用EJB 3.0持久化来实现原子对话。然而,Hibernate提供了这个特色,作为提供商对用规范定义的FlushModeTypes的扩充,并且我们也期待其它提供商会很快提供一个相似的扩充。
当开始一个对话时,Seam让你指定FlushModeType.MANUAL。现在,这个工作仅局限在Hibernate是底层持久化提供者时,但是,我们计划支持其它等价的提供商扩充。
@In EntityManager em; //a Seam-managed persistence context
@Begin(flushMode=MANUAL)
public void beginClaimWizard() {
claim = em.find(Claim.class, claimId);
}
现在claim对象通过持久化上下文对对话的其他部分维持受管理状态。我们能改变claim:
public void addPartyToClaim() {
Party party = ....;
claim.addParty(party);
}
但是这些变化将不被冲洗到数据库,除非我们明确地强迫冲洗发生:
@End
public void commitClaim() {
em.flush();
}
Of course, you could set the flushMode to MANUAL from pages.xml, for example in a navigation
rule:
当然,你能从pages.xml设置flushMode为MANUAL。例如,在一个导航控制:
<begin-conversation flush-mode="MANUAL" />
你能设置任何Seam受管理持久化上下文使用手动冲洗模式:
<components xmlns="http://jboss.com/products/seam/components"
xmlns:core="http://jboss.com/products/seam/core">
<core:manager conversation-timeout="120000" default-flush-mode="manual" />
</components>
9.4. 使用JPA "代理"
EntityManager接口让你通过getDelegate()方法访问一个提供商规范API。 自然,最感兴趣的提供商是Hibernate,并且最有威力的代理接口是org.hibernate.Session。使用任何其它的你都会发狂。相信我,我没有一点偏见。如果你使用了一个其它不同的JPA提供者,请看使用备用的JPA提供者。
但是无管你是否正在使用Hibernate(天才!)或者其它东西(受虐狂者,或仅有点愚顽者),你几乎肯定有时想在Seam组件中使用代理。下面是一个方法:
@In EntityManager entityManager;
@Create
public void init() {
( (Session) entityManager.getDelegate() ).enableFilter("currentVersions");
}
但在Java语言中分配角色无疑是丑陋的语法,所以大部分人无论何时尽可能避免他们。这儿有一个不同的方法得到代理。首先,增加下列行到components.xml:
<factory name="session"
scope="STATELESS"
auto-create="true"
value="#{entityManager.delegate}"/>
现在我们直接注入会话:
@In Session session;
@Create
public void init() {
session.enableFilter("currentVersions");
}
9.5.在 EJB-QL/HQL中使用EL
只要你使用了一个Seam受管理持久化上下文或使用@PersistenceContext注入了一个容器受管理持久化上下文,Seam 成为EntityManager 或 Session对象的代理器。这让你在你的查询字符串中安全、高效地使用EL表达式。例如,这个:
User user = em.createQuery("from User where username=#{user.username}")
.getSingleResult();
是等价于:
User user = em.createQuery("from User where username=:username")
.setParameter("username", user.getUsername())
.getSingleResult();
当然,你绝不能,写成这样:
User user = em.createQuery("from User where username=" + user.getUsername()) //BAD!
.getSingleResult();
(它是低效的,并易受到SQL注入攻击的攻击)
9.6.使用Hibernate过滤器
Hibernate最酷、最独特的特色是过滤器。过滤器让你提供一个受限的在数据库中的数据的视窗。在Hibernate文档中你能查到更多的有关过滤器的问题。
但是,我们认为我们提及了一个容易的方法,合并过滤器到一个Seam应用程序里面,其用Seam应用程序框架工作尤其好。
Seam受管理持久化上下文可以有一个定义的过滤器列表,只要EntityManager或 Hibernate Session第一次被创建,其会被激活。(当然,它们仅能在底层是Hibernate持久化提供者时被使用)