事务性 Web 流的持久化策略
Spring Web Flow 2 的 JPA/Hibernate 持久化架构建立在流管理的持久化概念基础上,这个概念过去只是简单提到过。在本文中,Xinyu Liu 将为您深入分析流管理的持久化的概念构建模块和流作用域的持久化上下文。然后,他将展示在复杂的真实世界场景中处理原子和非原子 Web 流的事务性策略。
Spring Web Flow 是一种新颖的 Java™Web 框架,它扩展了 Spring MVC 技术。使用 Spring Web Flow 的应用开发围绕着定义为 Web 流的用例展开。 将开发工作区根据 Web 流进行组织使开发体验更有意义、更具上下文。此外,Spring Web Flow 对 JPA/Hibernate 持久化的支持也是其最重要的服务器端改进之一。
尽管 SpringSource 和 Spring Web Flow 项目组详细介绍了 Spring Web Flow,但是其持久化支持,尤其是其流管理的持久化机制,很少为人所了解。本文将深入介绍 Spring Web Flow 2 中的 Java 持久化编程,重点讲解流管理的持久化及其基本组件 —流作用域的持久化上下文。
在概述 Spring Web Flow 持久化的基本概念之后,我将呈现几个用例,为大家展示处理原子和非原子 Web 流中的只读和读 / 写事务的策略。在每种情况下,我都将解释首选事务处理策略的基本思想并说明其缺点。本文结尾我为大家总结了在 Spring Web Flow 2 中高效、安全地管理事务的一些指导原则。
本文面向熟悉 Spring Web Flow 2 及其基于 continuations 的架构的经验丰富的 Java 开发人员。已经在 Spring Web Flow 中使用 JPA/Hibernate 的开发人员将从用例和样例应用程序代码中受益良多。
JPA/Hibernate 中的持久化挑战
在典型的 Web 应用程序中,有两个主要步骤处理用户请求:操作处理和视图呈现。应用程序的主要业务逻辑驻留在操作处理中。随后进行的视图呈现将数据提供给视图模板,将视图展现出来。
在 JPA/Hibernate 中,数据(更确切地说是实体关系)可能急切加载或延迟加载为代理对象。如果持久化上下文对象(JPA EntityManager
或 Hibernate Session
)在视图呈现阶段已经关闭,那么实体就会分离。任何访问分离实体上已卸载的关系的尝试都将导致LazyInitializationException
异常。
Open Session in View模式(请参见 参考资料)试图解决 LazyInitializationException
异常。当 Open Session in View 模式作为过滤器或拦截器实现时,持久化上下文对象在视图呈现期间会保持打开状态。导航到持久实体上的已卸载关系将触发其他的数据库查询来按需获取关系。
Open Session in View 模式的一个缺点是持久化上下文对象被高效地划定到用户请求作用域内。因此,存储在 Servlet 作用域中的实体,除当前请求外,总是被分离。分离的实体需要合并 / 重新连接 / 重新加载操作才能与当前持久化上下文关联。
Spring Web Flow 采用了不同的方法,它通过流管理的持久化,更确切地说是流作用域的持久化上下文对象,解决了分离实体状态的问题。
流管理的持久化
Spring Web Flow 中的应用程序开发基于 Web 流的概念,Web 流通常代表一个单独的用例。在很多情况中,整个 Web 流中的数据变化需要是原子的,也就是说流不同阶段的变化或者作为整体被保存到后端数据库中,或者全部取消,在数据库中不留下任何痕迹。
Spring Web Flow 通过 流管理的持久化机制简化了事务性原子 Web 流中的 JPA/Hibernate 编程。流管理的持久化在概念上与 Hibernate/Seam 对话一样(请参见 参考资料),其中在 Web 流(或者 Seam 中的 “页面流”)期间进行的数据变更都作为脏实体缓存在同一个流作用域的持久化上下文对象中。直到流结束时才会激活 SQL insert/update/delete 语句,将变更一次刷新并提交到数据库中。(注意,“刷新” 和 “提交” 是不同的概念;前者激活一系列 SQL insert/update/delete 语句使脏实体与其相应数据库值同步,而后者只是提交数据库事务)。
流管理持久化中的 OptimisticLockingFailureException
乐观锁是一个极其有效的并发性控制方法,它可以确保数据完整性而又无需在数据库中放置任何物理锁。尽管不是强制实施的,但是强烈建议在流管理的持久化中使用乐观锁。
持久化上下文在刷新时检查实体版本,如果检查出并发的修改,它就抛出 OptimisticLockingFailureException
异常(在 Hibernate 中是StaleObjectException
异常)。实体在内存中存在的时间越长,其对应的数据库值被其他进程修改的可能性越大。
如上所述,在 Open Session in View 模式中,实体的持久状态受制于用户请求。一旦实体分离,通常需要在后续用户请求中进行合并 / 重新连接 / 重新加载操作来还原实体的持久状态,从而使实体与其对应的数据库值保持同步。
在流管理的持久化中,实体在多个用户请求之间保存其持久状态。在各用户请求之间没有强制执行数据库同步,因此很有可能出现 OptimisticLockingFailureException
异常。重要的是要优雅地处理 OptimisticLockingFailureException
异常,就像处理任何检查型业务异常一样。(即使 OptimisticLockingFailureException
是一个回滚到数据库事务的运行时异常,也应如此)。常用的策略是让用户有机会合并其变更或用未过期数据重启流。
流作用域的持久化上下文
Web 流被声明为 XML 格式的流定义文件。带有 <persistence-context/>
标签的 Web 流启动时,会创建一个新的持久化上下文对象并将其绑定到流作用域。在等待用户请求时,该对象会断开与底层 JDBC 连接的连接并在服务于用户请求时重新连接。在整个流过程中都重用同一个持久化 - 上下文对象,这避免了分离实体状态的问题和相应的 LazyInitializationException
异常。
持久化上下文还被绑定到当前的请求线程并以两种方式公开给开发人员:作为隐式变量 persistenceContext
或通过 JPA@PersistenceContext
注释注入到任何 Spring bean 中。
隐式变量可以在流定义 XML 文件中直接得到,例如:
<evaluate expression="persistenceContext.persist(transientEntityInstance)"/>
注入的 JPA 实体管理器可以在 Spring 组件的任何地方引用,比如在 DAO 中、服务 bean 或 Web 层 bean 中。
持久化上下文的类型:事务性或扩展型
@PersistenceContext
注释有一个可选的属性 type
,该属性默认为 PersistenceContextType.TRANSACTION
(也就是绑定到事务的持久化上下文)。在用流作用域的持久化上下文编程时必须使用此默认设置。在这种情况下,注入的绑定到事务的持久化上下文对象只是一个共享的代理,它透明地委托给了绑定到线程的实际的流作用域的持久化上下文。
选择另一个选项,也就是 PersistenceContextType.EXTENDED
,会得到一个叫做 “扩展的实体管理器” 的东西,它对线程而言是不安全的,不能在并发访问的组件,比如单态 Spring bean,中使用。将扩展的实体管理器用作流作用域的持久化上下文会导致应用程序中出现不可预测的数据库 / 事务行为,因此要尽量避免使用它。
有趣地是,Seam 对话通常用注入到有状态会话 bean (EJB) 中的扩展的实体管理器实现。这是 Spring Web Flow 的流管理的持久化和 Seam 对话之间的一个显著区别。
流作用域的持久化上下文对象可以与 @Transactional
注释一起使用以调整流的持久化特征。
事务语义
作为 Spring 核心包一部分的 @Transactional
注释指定了注释的类或方法的事务语义。根据 Spring 开发团队所述,@Transactional
最好应用于具体类而不是接口。默认事务语义是:
@Transactional(readOnly=false,propagation=PROPAGATION_REQUIRED, isolation=ISOLATION_DEFAULT,timeout=TIMEOUT_DEFAULT)
readOnly:通过指定 @Transactional(readOnly=false)
建立读 / 写事务,这样会使持久化上下文的 FlushMode
变为 AUTO
。应用@Transactional(readOnly=true)
会使底层 Hibernate 会话的 FlushMode
变为 MANUAL
。
JPA 1.0 不支持 MANUAL
刷新以及只读事务,因此只有在底层 JPA 提供商,比如 Hibernate,支持只读数据库事务时,@Transactional(readOnly=true)
才有意义。而且,Hibernate 将此设置用作针对某些数据库类型的数据库提示从而优化查询性能。
propagation:propagation
属性确定当前方法是在继承的事务下运行, 还是通过挂起 / 继续封闭事务在新事物中运行,或者根本没有在事务中运行。
isolation:JPA 1.0 不支持自定义隔离级别,因此开发人员需要指定数据库端的默认事务隔离级别。Read-Committed
是乐观锁工作所需的最低级别。
timeout:timeout
属性指定在超时(以及被底层事务基础设施自动回滚)之前事务可以运行多长时间 。
rollbackFor、rollbackForClassname、noRollbackFor、noRollbackForClassname: 一般而言,在出现表示系统错误的RuntimeException
异常时事务总是回滚,在遇到带有预定义业务意义的检查型 Exception
时总是会提交。可以通过这 4 个回滚属性自定义默认语义。
Spring 核心包的健壮的事务基础设施使绝大部分真实开发场景中的事务管理更加轻松。在下面各节中,我们将了解 Spring Web Flow 如何利用 Spring 事务基础设施连同其自己的流作用域的持久化上下文对象来处理各种 Web 流中的持久化编程,包括一些展示了流管理持久化的局限性的用例。
原子 Web 流
流管理持久化旨在处理那些从事务角度来说属于原子性的 Spring Web Flow 用例。例如,假设有一个网银系统,该系统允许用户将资金从支票账户移动到储蓄账户或即将建立的定期账户。事务必须分几个步骤完成:
- 用户选择要转账的支票账户。
- 系统显示账户余额。
- 用户输入要转账的金额。
- 用户选择一个储蓄或定期账户作为目标。
- 系统显示整个交易的摘要供用户检查。
- 用户决定提交或取消交易。
由于明显的并发性需求,您应该首先在实体类上启用乐观锁。为此,您可以使用 JPA @Version
注释或者 Hibernate 私有OptimisticLockType.ALL
属性。然后将整个用例映射到一个带有 Spring Web Flow 的 <persistence-context/>
标签的 Web 流中。
Web 流中非事务性数据访问
在 Spring Web Flow 中,默认情况下,所有数据访问都是非事务性的。对于非事务性数据访问,Hibernate 将底层数据库的 auto-commit
模式设置为 true
,这样每个 SQL 语句都会在其自己的 “短事务” 中立即执行,提交或回滚。从应用程序的角度而言,数据库短事务等效于根本没有事务。更为严重的是,对于非事务性操作,Hibernate 禁用了默认的 FlushMode.AUTO
。它在 FlushMode.MANUAL
模式下高效地工作。
禁用 FlushMode.AUTO
对于流管理的持久化而言很重要。视图呈现阶段的实体延迟读取也在非事务性模式下执行。如果在呈现不同视图期间发生过刷新,那么就无法在流末尾完成延迟的刷新。在本质上,auto-commit
模式中的非事务性读取等效于隔离级别为 Read-Committed
的事务内的读取。类似地,非事务性写操作永远不会进行刷新。
在上述用例中,每个用户操作都可以在数据库事务之外执行,无需指定 @Transactional
注释或 XML 配置的事务顾问。 流作用域的持久化上下文对象将流期间加载的数据作为持久化实体来管理并将数据变更缓存为实体的脏状态。
如果用户在流末尾通过 <end-state
确认了转账交易,那么 Spring Web Flow 运行时将在读 / 写数据库事务内隐式地调用
commit="true"/>entityManager.flush()
。然后提交事务,取消绑定持久化上下文并关闭它。如果用户选择通过 <end-state commit="false"/>
取消了交易,那么所有缓存的数据变更都会在关闭流作用域的持久化上下文之际在内存中被丢弃。
流管理持久化使用的这种方法与 JPA 1.0 解释对话处理的方式是完全一致的。JpaFlowExecutionListener
类是使这一切发生的底层 Spring Web Flow 组件。除了流管理持久化的非事务性数据访问方法之外,还可以使用只读事务。
Web 流中的只读事务
在某些情况下,与非事务性事务相比您可能更愿意使用只读事务。如果查看 Spring Web Flow 发行版中的样例 “Hotel Booking” 应用程序(请参见 参考资料),会注意到在整个 “booking” Web 流期间,在全局范围内,对所有数据访问都使用了 @Transactional(readOnly=true)
,无论操作的本质如何(读 / 插入 / 更新 / 删除)。
JPA 1.0 规范不支持只读事务,因此只有在某些 JPA 提供商中才能使用此设置。在其 JPA 实现中,Hibernate 将底层 Hibernate 会话的FlushMode
设置为 MANUAL
并将 auto-commit
模式设置为 false
。
流管理持久化的只读事务的表现与非事务性数据访问一样,只有在原子 Web 流结尾才会通过 <end-state
刷新变更的实体。
commit="true"/>
如果希望刷新发生在 <end-state/>
之前,您需要在用 @Transactional
注释的 Spring bean 方法之一中调用 entityManager.flush()
。
直接从 Web 流调用,<evaluate
,行不通,因为没有事务绑定到任何 Spring Web Flow 标签,除了
expression="persistenceContext.flush()"/><end-state commit="true"/>
。您会得到以下错误消息:
"javax.persistence.TransactionRequiredException: no transaction is in progress"
本文稍后我们会回到 “Hotel Booking” 这个示例,来了解 没有流作用域持久化上下文的持久化编程 所面临的挑战。
关于事务传播的更多信息
我已经介绍了事务如何根据其 propagation
属性的值进行传播,但是我忽略了一个特别的用例:如果标有 @Transactional(readOnly=true, propagation=Propagation.REQUIRED)
的方法要调用另一个标有 @Transactional(readOnly=false, propagation=Propagation.REQUIRED)
的方法,或者情况相反, 那么事务又将如何传播呢?
Spring Web Flow 用一种简单而聪明的方式处理了这个问题:它忽略了第二个方法上的 readOnly
属性值。简而言之,初始化为只读的事务会保持只读状态,直到它结束,而且 反之亦然。
这对于在流管理的持久化中是不使用事务还是使用只读事务的问题产生了有趣的影响。
只读事务的一个用例
应用程序服务层的 Spring bean 可以通过一些 JAX-WS/JAX-RS 注释公开为可重用的 SOAP/REST Web 服务。在这些 @Service
bean 或其方法中应用 @Transactional
将 Web 服务调用与数据库事务绑定到了一起(没有明显的原因要在 DAO @Repository
bean 上使用@Transactional
,除非应用程序具有级联式层架构,其中没有其他地方供开发人员指定事务属性)。
再思考一下 Spring Web Flow 中流管理持久化的非事务性数据访问方法。如果将 @Transactional
应用到启用了 Web 服务的 @Service
bean,则非事务性上下文可能被覆盖。流作用域持久化上下文中的所有未决数据变更都会在方法调用链中遇到在服务层指定的读 / 写事务时刷新,这将导致所谓的 “提前刷新”。
避免提前刷新
如果在插入期间生成实体标识符(即标识列),即使在手动刷新情况下,刷新发生在调用entityManager.persist()
或entityManager.merge()
方法后。这很有必要,因为持久化上下文中的每个托管(持久化)实体都必须被指定一个标识符。要避免这种提前刷新,需要将 ID 生成策略设置为 sequence
。
另一方面,在视图层 Spring bean 上指定 @Transactional(readOnly=true)
将覆盖服务 bean 上的读 / 写事务设置;事务将保持只读状态以防止提前刷新。在 SOAP/REST Web 服务通信中绕过整个 Web 层的情况下,应用到服务 bean 的 @Transactional
注释确保 Web 服务调用运行在数据库事务内。
这是在流管理持久化中使用只读事务优于使用非事务性数据访问的地方。
如上所示,流管理持久化解决了涉及原子 Web 流的用例。本文剩余部分将重点介绍调用非原子 Web 流的用例,其中未应用流管理持久化。注意在其中的某些用例中我们仍然能够使用流作用域的持久化上下文对象。
非原子 Web 流
从业务流程管理 (BPM) 角度而言,一种长期运行的进程比典型的 Web 会话存活时间长。如果此类长期运行的进程涉及了人类的任务, 则用户可以在该进程中工作任意长的时间,并且可以在几小时、几天甚至几个月之后回来恢复进程的执行。显然,此类进程应该在服务器崩溃时也可以存活。
所有这些因素都表明每一次进程运行后长期进程的状态都需要持久保存到后端数据库。将此长期运行进程的人类活动实现为 Web 流成为一个明智的技术解决方案。流将在不同的 Web 会话中重复执行以模仿长期进程的生命周期。
除了上述场景,还有一些应用程序由非上下文 Web 页组成,用户可以在这些 Web 页之间任意导航。这些 Web 页可以根据其业务功能被分组成流,即使没有逻辑顺序流也没有开始或结束状态。每个用户请求期间所做的数据变更都要被保存。这些应用程序中的持久化编程与上述长期运行的进程没有什么不同,事务原子性的作用域都划定到每个用户操作而不是一系列用户操作 —一个 Web 流。
非原子 Web 流用例
在医疗卫生行业,服务提供商定期接触患有慢性病的成员以评估他们的健康状况和潜在风险。健康提供商随后为其提供治疗和行为健康方面的建议。这称为 案例管理。
案例管理系统围绕着一系列联系任务。在一个典型任务中,案例经理会通过电话联系一个成员,询问评估问题并根据其回答给出适当的建议、创建转诊请求、记录联系结果和设置后续任务。
情况非常复杂。评估问题清单可能很长:电话可能由于各种原因而被中断、没有记录转诊某些任务可能无法完成等等。包含并发或异步操作的联系任务是一个长期运行的进程,每一步进展都要被保存到数据库。联系任务可以被模拟为单一的 Web 流,在长期运行进程的发展过程中,它可以被重复进入和执行。
Spring Web Flow 文档没有介绍此非原子 Web 流场景。在此用例中仍然可以利用流作用域的持久化上下文对象吗?答案是可以。
指定事务的作用域
我们知道流定义文件中的 <persistence-context/>
标签为我们提供了一个绑定到线程的 flowScoped
持久化上下文,这带来了 没有分离实体和 没有LazyInitializationException
异常的好处。因此,我们选择保留这个标签。与原子流中流管理的持久化相比,事务作用域发生的最大变化时:原子性应用于进程的每一步而不是整个流。通常,进程中的原子步骤是由 Web 流定义中的 <transition>
标签表示的一个用户操作。
令人失望的是 Spring Web Flow 在其任何标签上都不支持事务分界,包括 <transition>
和 <evaluate>
。开发人员的下一个选择是从注释有@Transactional
的 Spring bean 方法发起数据库事务并从 <evaluate>
标签调用该方法(<transition>
标签不支持方法调用)。
从本质上来说,事务的作用域划定到流中的 <evaluate>
标签。应用 @Transactional(readyOnly=false)
将使 JPA/Hibernate FlushMode
,这样 Hibernate 会确定在同一事务的上下文内何时刷新数据变更。为了简化编程和优化 SQL,在这些用例中使用自动刷新要优于手动刷新。注意在同一
设置为 AUTO<transition>
下允许多个 <evaluate>
标签,会导致每用户操作出现多个数据库事务。
如果每个用户操作 / 请求被认为是原子性的,通常事实也是如此,我们希望将所有数据库写操作分组到一个 Spring bean 的一个@Transactional
方法内,这样它们就绑定了同样的事务上下文并通过同样的 <evaluate>
标签调用。清单 1 展示了我们如何指定原子请求的事务上下文。
清单 1. 指定原子用户操作的事务上下文
<transition> <evaluate expression="beanA.readAlpha()"/> <evaluate expression="beanA.readBeta()"/> <evaluate expression="beanB.readGamma()"/> <evaluate expression="beanA.writeAll()"/> <!-- a single read/write transaction --> <evaluate expression="beanB.readEta()"/> </transition>
清单 2 展示了一个非典型的案例,其中同一个用户请求中涉及了多个读 / 写事务(独自提交或回滚)。因此,用户请求变成了非原子的,这在大部分开发场景中是灾难性的。
清单 2. 为非原子用户操作指定事务上下文
<transition> <evaluate expression="beanA.readAlpha()"/> <evaluate expression="beanA.readBeta()"/> <evaluate expression="beanB.readGamma()"/> <evaluate expression="beanA.writeDelta()"/> <!-- read/write transaction --> <evaluate expression="beanA.writeEpsilon()"/> <!-- read/write transaction --> <evaluate expression="beanB.writeZeta()"/> <!-- read/write transaction --> <evaluate expression="beanB.readEta()"/> </transition>
我们如何处理那些同一 <transition>
下的其他 <evaluate>
标签引用的只读操作?我们有三个选择:
- 如前所述,不带有任何数据库事务地运行只读操作。
- 将其标记为
@Transactional(readOnly=false)
,这样可以在读 / 写数据库事务下执行 SQL 查询。在这种情况下,流作用域持久化上下文的FlushMode
将一直是AUTO
。 - 用
@Transactional(readOnly=true)
标记它们。这样,对于那些只读事务FlushMode
变成MANUAL
而在遇到读 / 写事务时又会过渡到AUTO
。
在持久化上下文中,JPA/Hibernate 会在提交读 / 写事务前自动刷新未决变更。为了简单,Hibernate 团队鼓励开发人员在此类情况下,在所有数据操作中都一致地应用读 / 写事务。只需向所有应用 @Transactional
的地方设置 readOnly=false
即可。
每请求一个事务的情况如何?
您可能会问为什么不用一个大事务封装每个用户请求。问题是请求级别的事务会使相关的数据库锁保持打开状态,直到完成视图呈现。不幸地是,视图呈现涉及阻止 I/O 操作,这可能会很慢而影响底层数据库的性能和可伸缩性。
意料之外的 OptimisticLockingFailureException 异常
在非原子 Web 流中使用流作用域的持久化上下文,您可能会遇到一些意料之外的OptimisticLockingFailureException
异常。
对于非原子 Web 流还是强烈推荐使用乐观锁来保护每个用户操作的数据完整性。实体的@Version
字段是一个数据库生成的整数或时间戳且随后伴有更新操作时,需要显式地查询实体以便在持久化上下文中刷新其状态。否则,@Version
字段将带有陈旧的值而后续在不同事务中对同一实体的更新会导致 OptimisticLockingFailureException
异常。具有讽刺意味的是,这个异常将在没有多用户并发性的情况下发生。相反,在原子流中必须避免这种更新后查询,否则将发生提前刷新。毕竟,无论在原子 Web 流期间,在内存中对实体对象更新了多少次,流结尾处发生的 SQL 刷新只能看到实体实例的最后状态。
显然流作用域持久化上下文使原子和非原子 Web 流中的持久化编程更顺利、更简单。不使用流作用域持久化上下文对象的 Web 流中的持久化编程也是可行的,只是有很多障碍和缺陷。
不使用流作用域持久化上下文的持久化编程
在某些情况下,如 Hotel Booking 样例应用程序所示,可以在没有 <persistence-context/>
标签的情况下实现 Web 流。这种方法最明显的影响在原子 Web 流上,一旦省略了流作用域持久化上下文对象您就无法得到原子 Web 流了。我将在下文讨论其他不便之处。
持久化上下文作用域划定到数据库事务
不使用流作用域持久化上下文,通过 @PersistenceContext
注释注入的持久化上下文默认情况下作用域划定到数据库事务。要更好地理解这种操作的问题所在,请查看来自 Hotel Booking 样例应用程序的以下代码片段:
清单 3. Hotel Booking 中 “主流” 定义的代码片段
<view-state id="enterSearchCriteria"> <on-render> <evaluate expression="bookingService.findBookings(currentUser.name)" result="viewScope.bookings" result-type="dataModel" /> </on-render> <transition on="cancelBooking"> <evaluate expression="bookingService.cancelBooking(bookings.selectedRow)" /> <render fragments="bookingsFragment" /> </transition> </view-state>
如果清单 3 中引用的 cancelBooking
方法定义如下 :
清单 4. cancelBooking 方法
@Service("bookingService") @Repository public class JpaBookingService implements BookingService { //... @Transactional public cancelBooking(Booking booking){ if (booking != null) { em.remove(booking); } }
那么运行此代码时我们将得到如下错误:
Caused by: java.lang.IllegalArgumentException: Removing a detached instance
<on-render>
标签返回的 booking
实体会在后续操作 <transition on="cancelBooking">
中分离。同一 bookingService
bean 的两个方法findBookings
和 cancelBooking
在不同的数据库事务下执行,因此与两个截然不同的持久化上下文对象相关联。一个持久化上下文管理的booking
实体,从另一个持久化上下文的角度来看,是分离的。
为了规避这一问题,在清单 5 所示的实际 cancelBooking
方法中,同一 booking
实体在它被删除前通过其主键被重新加载。
清单 5. 修复的 cancelBooking 方法
@Service("bookingService") @Repository public class JpaBookingService implements BookingService { //... @Transactional public cancelBooking(Booking booking){ booking = em.find(Booking.class, booking.getId()); // reinstate the persistent entity if (booking != null) { em.remove(booking); } }
以事务为作用域的持久化上下文与带有 singleSession=false
的 OpenSessionInViewFilter
/Interceptor
的工作方式一样。 这意味着同一请求中的每个事务都有其自己的相关会话。但是这里我们失去了 Open Session in View 的 “延迟关闭模式” 的优势。
在视图呈现期间,延迟读取将导致 LazyInitializationException
异常,因为以事务为作用域的持久化上下文会在每个事务完成后立即关闭。也可以选择实现类似于 OpenSessionInViewInterceptor
/ OpenEntityManagerInViewInterceptor
之类的东西,但是 Spring 核心包提供的那些东西不能开箱即用地为 Spring Web 工作。使用内置的流作用域的持久化上下文对象要简便得多!
作用域为每个调用的持久化上下文
没有流作用域持久化上下文协助的非事务性数据访问是情况最糟糕的场景,应该尽量避免。
事务之外,持久化上下文的作用域被划定到带有 FlushMode
和
AUTOauto-commit true
设置的每个调用(记住 Hibernate 为非事务性数据访问禁用了自动刷新)。换句话说,对通过 @PersistenceContext
注入的同一持久化上下文代理的每个方法调用都将返回一个不同的实体管理器实例,且这些实例会立即打开而后关闭。
从本质上来说,实体管理器的作用域划定成了 “短事务”。您在 清单 5中看到的同样的代码片段将会发出以下的错误消息:
java.lang.IllegalArgumentException: Removing a detached instance
在不同的流中传递实体
这是最后一个有时会引起问题的场景:需要在不同流中传递实体时会出现什么情况?
流作用域的持久化上下文对象服从于流的作用域,因此一旦从一个流向另一个流传递实体,这些实体就会立即分离。解决方法是将这些实体合并 / 重新连接到当前流的持久化上下文或者用其主键重新加载它们,这种策略仿效了 Open Session in View 方法。
结束语
Spring Web Flow 是一种先进的 Web 开发框架,提供了独特的功能以支持使用 JPA/Hibernate 的持久化编程和事务管理。本文探讨了 Java 开发人员在编写 Spring Web Flow 应用程序中面临的困难和挑战。
从真实世界用例中(比如本文介绍的用例),对于在 Spring Web Flow 中编码事务性原子和非原子 Web 应用程序,我总结出了以下 “规则”:
- 首选的是使用流作用域持久化上下文
- 全局性地将只读事务应用到原子 Web 流中引用的所有方法
- 全局性地将读 / 写事务应用到非原子 Web 流中引用的所有方法