Chapter 8. Seam和对象/关系映射

Seam给两个最流行的Java持久化架构:Hibernate3和由EJB 3.0引入的Java Persistence API提供了广泛支持。 Seam独有的状态管理架构允许任意web应用框架与大多数成熟的ORM进行集成。

8.1. 简介

Seam是从Hibernate团队试图生成典型的无状态Java应用架构的挫折中成长起来的。 上一代Java应用程序的无状态特性让Hibernate团队饱受挫折,Seam吸取了他们的经验。 Seam的状态管理架构最早是用来解决持久化冲突相关问题的,特别是 乐观事务处理 相关的问题。可扩展的在线应用经常使用乐观事务。 一个原子(database/JTA)级的事务不应该跨用户交互,除非系统设计时就是只支撑很少量的并发客户端。 但几乎所有涉及到的工作都是先将数据展现给用户,没多久后更新这个数据。所以Hibernate是依据支持一种跨乐观事务的持久化上下文的思想设计的。

不幸的是这个先于Seam和EJB3.0出现的所谓“无状态”架构并不对乐观事务进行支持。而相反,这些架构提供对于原子事务级的持久化上下文的支持。 这当然给用户带来了很多麻烦,这也是用户抱怨排名第一的Hibernate的 LazyInitializationException 问题的原因。 我们需要的是在应用层构建对于乐观事务的支持。

EJB3.0认识到了此问题,并且也引入了有状态组件(有状态会话bean)的思想,它使用一个 扩展持久化上下文来跟踪组件的生命周期。 这是该问题的部分解决方案(对它自身而言也是一个有用的构想),然而还有两个问题:

  • 有状态会话bean的生命周期必须在Web层通过代码手动管理(这是个麻烦的问题,而且实践起来比听上去更复杂)。

  • 在同一个乐观事务的不同有状态组件间,传播持久化上下文是可行的,但很困难。

Seam通过提供对话(Conversation)和对话期间的有状态Session Bean组件来解决第一个问题(大多数会话实际上在数据层支持乐观事务)。 这对于很多不需要传递持久化上下文的简单应用(比如Seam的订阅演示程序)已经足够了。 对于更复杂的在每一个对话中的有很多松耦合组件的应用来说,组件间传播持久化上下文就成为一个重要的问题了。 所以Seam扩展了EJB 3.0的持久化上下文管理模型,以此来提供对话作用域的扩展持久化上下文。

8.2. Seam管理的事务

EJB会话Bean有声明式事务管理功能。当Bean被调用时,EJB容器能够透明地开始一个事务,在调用结束时关闭此事务。 如果我们写了一个作为JSF动作监听器的会话Bean方法,我们就可以在一个事务内处理所有与此action相关的工作,并且当我们完成此动作处理时事务必须被提交或回滚。 这是一个很棒的功能,在很多Seam应用程序中这是必需的。

但是,此方法还是有问题。Seam应用可能无法在对会话Bean的一次方法调用请求中完成所有的数据访问。

  • 此请求可能由几个松耦合组件处理,Web层独立地调用每一个组件。在Seam中,Web层的一个请求对EJB组件发起几次甚至多次调用的现象是很常见的。

  • 视图渲染可能需要延迟关联获取(lazy fetching of associations)。

每个请求的事务量越多,当我们的应用处理大量并发请求时越可能碰到原子和隔离问题。当然,所有的写操作要在一个事务中执行。

Hibernate用户开发了 "Open Session in View" 模式来解决该问题。 在Hibernate社区,"Open Session in View"曾经非常重要,这是因为像Spring这样的框架使用了事务作用域持久化上下文。 所以当未获得的关联被访问时渲染视图将引起 LazyInitializationException 异常。

这个模式通常作为一个跨越整个请求的事务来实现。 此实现方式会有几个问题,其中最严重的是只有我们提交了事务才能确认它成功完成——但在"Open Session in View"的事务提交时,视图已经完全渲染了,甚至渲染好的应答可能已经刷新到客户端。我们怎样才能通知用户他们的事务已失败呢?

Seam在解决"Open Session in View"问题时,也解决了事务隔离和关联获取问题。该方案有有两个部分:

  • 使用使用已扩展持久化上下文,可以覆盖一个会话作用域而不是单个事务作用域。

  • 每次请求使用两个事务;第一个从更新模型值的起始阶段到应用程序调用结束;第二个跨越渲染响应阶段。

下一节,我们将会告诉你如何安装一个会话作用域的持久化上下文。但首先我们需要你知道如何启用Seam事务管理。 注意你可以脱离Seam的事务管理来使用会话作用域持久化上下文。当你不使用Seam管理的持久化上下文时,你也有很多使用Seam事务管理的理由。 然而这两种功能被设计为一起使用的,一起使用时效果最好。

即使你使用EJB 3.0容器管理事务上下文,Seam事务管理也是很有用的。如果你在Java EE 5环境外使用Seam,或者在任何你想使用Seam管理的持久化上下文时,它同样很有用的。

8.2.1. 关闭Seam管理的事务

所有的JSF请求默认开启Seam事务管理。如果你想 关闭 该功能,你能在 components.xml 文件中做如下设置:

<core:init transaction-management-enabled="false"/>

<transaction:no-transaction />

8.2.2. 配置Seam事务管理器

Seam为事务的开始,提交,回滚,同步提供了一个事务管理抽象。默认情况下,Seam使用一个JTA事务组件,它同容器管理的EJB和编程式EJB事务集成。

Seam还为以下事务API提供事务组件:

  • javax.persistence.EntityTransaction 接口提供的JPA RESOURCE_LOCAL事务。
  • org.hibernate.Transaction 接口提供的Hibernate管理的事务。
  • org.springframework.transaction.PlatformTransactionManager 接口提供的Spring管理的事务。
  • 显式关闭Seam管理的事务。

向components.xml文件增加以下项来配置JPA RESOURCE_LOCAL事务,在配置文件中,#{entityManager}persistence:managed-persistence-context 组件的名称。(参考 Seam管理的持久化上下文。)

<transaction:entity-transaction entity-manager="#{entityManager}"/>

向你的components.xml文件声明以下项来配置Hibernate管理的事务,在配置文件中,#{hiberanteSession} 是项目中 persistence:managed-hibernate-session 组件的名称。(参考Seam管理的持久化上下文

<transaction:hibernate-transaction session="#{hiberanteSession}"/>

在components.xml中声明以下内容来显式地关闭Seam管理的事务:

<transaction:no-transaction />

参考 使用Spring PlatformTransactionManagement 来配置Spring管理的事务。

8.2.3. 事务同步

事务同步为各种各样的事务相关事件提供了回调功能,例如 beforeCompletion()afterCompletion() 事件。 默认情况下,Seam使用它自己的事务同步组件,一个事务被提交时,需要显式地使用Seam事务组件以确保同步回调被正确执行。 如果在Java EE 5环境里,应该在components.xml文件中声明 <transaction:ejb-transaction/>,从而保证容器在Seam的可预见范围外提交事务时,Seam同步回调被正确调用。

8.3. Seam管理的持久化上下文

如果你是在Java EE 5环境外使用Seam,你不能依靠容器来为你管理持久化上下文生命周期。 即使在Java EE 5 环境中,你可能有一个很多松耦合组件在会话作用域内相互协作的复杂应用,这种情况下你可能发现在组件间传递持久化上下文既困难又容易出错。

在任何一种情况下,你都需要在你的组件中使用一个 受管持久化上下文(在JPA中)或者一个 受管会话(Hibernate中)。一个Seam管理的持久化上下文是在会话上下文中管理一个 EntityManager 实例或者 Session 实例的内置Seam组件。你可以使用 @In 注入它。

Seam管理的持久化上下文在集群环境中尤其有效。EJB 3.0规范中不允许容器使用容器管理的扩展持久化上下文,Seam能够对此进行优化。 Seam支持扩展持久化上下文的透明故障恢复,而无需在节点间复制持久化上下文状态。(我们希望在EJB规范的下个版本中修复此漏洞。)

8.3.1. 在Seam管理的持久化上下文中使用JPA

配置一个Seam管理的持久化上下文很简单,在 components.xml 中加上:

<persistence:managed-persistence-context name="bookingDatabase"
                                  auto-create="true"
                   persistence-unit-jndi-name="java:/EntityManagerFactories/bookingData"/>

这个配置创建了一个名为 bookingDatabase 的对话作用范围Seam组件,它管理给持久化单元(EntityManagerFactory 实例)使用的 EntityManager 实例的生命周期。 其JNDI名为 java:/EntityManagerFactories/bookingData

当然,你需要确认已经在JNDI中绑定了 EntityManagerFactory。 在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来访问实体管理器。

8.3.2. 使用Seam管理的Hibernate会话

Seam管理的Hibernate sessions和前者很相似。在 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 name="transaction.manager_lookup_class">org.hibernate.transaction.JBossTransactionManagerLookup</property>
    <property name="transaction.factory_class">org.hibernate.transaction.JTATransactionFactory</property>
    <property name="connection.datasource">java:/bookingDatasource</property>
    ...
</session-factory>

注意:Seam并没有清空此会话,所以你应启用 hibernate.transaction.flush_before_completion,确保在JTA事务提交前会话被自动地清空缓存。

现在我们可以用以下代码向JavaBean组件中注入一个受管Hibernate Session了:

@In Session bookingDatabase;

8.3.3. Seam管理的持久化上下文和原子会话

会话期间的持久化上下文让你能编写跨越多个服务器请求的乐观事务,而且无需使用merge()操作,也不需要在每次请求开始时重载数据,也不需要处理 LazyInitializationException 异常或 NonUniqueObjectException 异常。

对任何乐观事务管理来说,事务隔离性和一致性可以使用乐观锁来获得。 幸运的是,Hibernate和EJB 3.0都通过使用 @Version 注解来使用乐观锁。

默认情况下,持久化上下文在每个事务结束时会清空缓存(与数据库同步)。有时这是我们期望的方式。 但经常我们希望所有的改变在内存中保存,且只有在会话成功结束时写回数据库。这对于正真的原子会话来说是允许的。 作为非JBoss,非Sun,非Sybase的EJB 3.0专家组成员的愚蠢和短视决策的结果,使用EJB 3.0持久化还不能简单、有效和方便的实现原子会话。 但是Hibernate通过扩展规范定义的 FlushModeType 提供了这个功能,我们也期望其它的厂商能尽快提供类似扩展。

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();
}

当然你可以在pages.xml文件中将 flushMode 值设置为 MANUAL,例如在一个导航规则中写入:

<begin-conversation flush-mode="MANUAL" />

8.4. 使用JPA “代理(delegate)”

EntityManager 接口能让你通过使用 getDelegate() 方法访问某个厂商特定的API。 很自然地,最让人感兴趣的厂商是Hibernate,其最强大的代理接口是 org.hibernate.Session。 你不必再用别的东西,相信我,我没有任何偏见。

不管你使用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");
}

8.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注入攻击时会变得低效且不堪一击。)

8.6. 使用Hibernate过滤器

Hibernate最酷最独特的功能就是 过滤器(filter)。过滤器能让你提供一个数据库中数据的受限视图。 但是应该有一个将过滤器合并到Seam应用中的简便方法,让它和Seam应用框架能工作得很好。

Seam管理的持久化上下文可以定义一系列的过滤器,这些过滤器在一个 EntityManager 或者 Hibernate Session 被创建时启用。(当然,它们只能在Hibernate做持久化底层时使用)

<persistence:filter name="regionFilter">
    <persistence:name>region</persistence:name>
    <persistence:parameters>
        <key>regionCode</key>
        <value>#{region.code}</value>
    </persistence:parameters>
</persistence:filter>

<persistence:filter name="currentFilter">
    <persistence:name>current</persistence:name>
    <persistence:parameters>
        <key>date</key>
        <value>#{currentDate}</value>
    </persistence:parameters>
</persistence:filter>

<persistence:managed-persistence-context name="personDatabase"
    persistence-unit-jndi-name="java:/EntityManagerFactories/personDatabase">
    <core:filters>
        <value>#{regionFilter}</value>
        <value>#{currentFilter}</value>
    </core:filters>
</persistence:managed-persistence-context>