首页 新闻 论坛 群组 Blog 文档 下载 读书 Tag 网摘 搜索 开源 FAQ 第二书店 博文视点 程序员
频道: 研发 数据库 中间件 信息化 视频 .NET Java 游戏 移动 服务: 人才 外包 培训
    图书品种:235680
       
热门搜索: ASP.NET Ajax Spring Hibernate Java
事务和并发

本章内容

l    数据库事务

l    使用Hibernate和Java Persistence的事务

l    非事务的数据访问

本章终于讨论事务以及如何在应用程序中创建和控制工作单元了。我们将介绍事务如何在最低级别(数据库)工作,以及在基于原生Hibernate、Java Persistence和用或者不用Enterprise JavaBeans的应用程序中如何处理事务。

事务允许你设置工作单元的范围:一个原子的操作组。它们还帮助你在多用户的应用程序中把一个工作单元从另一个中隔离出来。我们讨论并发,以及如何在应用程序中利用悲观和乐观的策略控制并发的数据访问。

最后,看一下非事务的数据访问,以及什么时候应该在自动提交模式下使用数据库。

10.1  事务本质

从一些背景信息开始。应用程序功能要求同时完成几件事情。例如,当一次拍卖结束时,CaveatEmptor应用程序必须执行3项不同的任务:

(1) 给胜出(金额最高)的出价做标记。

(2) 向卖主收取拍卖费用。

(3) 通知卖主和成功的出价人。

如果你由于外部信用卡系统的失败而无法收取拍卖费用,会发生什么事?业务需求可能规定,列出的所有动作必须要么都成功,要么都不成功。如果这样,你可以把这些步骤全部称为事务(transaction)或者工作单元。哪怕只有一个步骤失败,则整个工作单元都必定失败。这就是大家所知的原子性(atomicity),即所有操作都作为一个原子单元来执行。

此外,事务允许多个用户同时使用相同的数据,而不破坏数据的完整性和正确性;特定的事务不应该对其他同时运行的事务可见。为了完全理解这个隔离性(isolation)行为,有几个策略很重要,我们将在本章中给予探讨。

事务具有其他重要的属性,例如一致性(consistency)和持久性(durability)。一致性意味着事务始终工作在同一组数据上:从其他同时运行的事务中隐藏起来的、并在事务完成之后留在一个清洁和一致的状态中的一组数据。数据库完整性规则保证一致性。你也想要事务的正确性(correctness)。例如,业务规则规定向卖主收取一次费用,而不是两次。这是个合理的假设,但是你可能无法用数据库约束把它表达出来。因而,事务的正确性是应用程序的责任,而一致性则是数据库的责任。持久性意味着一旦事务完成,所有变化都在该事务变成持久化期间进行,即使系统后来失败了,这些变化也不会丢失。

把这些事务属性归结起来,就是大家所知的ACID标准。

数据库事务必须简短。单个的事务通常只涉及单批数据库操作。在实践中,还需要一个概念,允许你有长期运行的对话,在这里一个原子组合的数据库操作不是在一批而是几批中发生。对话允许应用程序的用户有思考时间,而仍然保证原子、隔离且一致的行为。

既然已经定义了术语,就可以讨论事务划分(demarcation)以及如何定义一个工作单元的范围了。

10.1.1  数据库和系统事务

数据库把工作单元的概念实现为一个数据库事务(database transaction)。数据库事务组合了数据库访问操作——也就是SQL操作。所有SQL语句都在一个事务内部执行;无法把SQL语句发送到数据库事务之外的数据库。事务被确保以这两种方式之一终止:要么完全被提交(commit),要么完全被回滚(roll back)。因而,我们说数据库事务是原子的。在图10-1中,你可以看到这个图示。

回滚

 

事务失败

 

事务完成

 

提交

 

开始

 

初始状态

 

事务

 

图10-1 一个原子工作单元(事务)的生命周期

为了在事务内部执行所有的数据库操作,必须给这个工作单元的范围做标记。必须启动事务,并在某个时间点提交变化。如果出现错误(在执行操作或者提交事务的时候),就必须回滚事务,把数据留在一致的状态中。这就是大家所知的事务划分,取决于你所使用的技术,它涉及更多或者更少的手工干涉。

一般来说,启动和终止事务的事务范围可以在应用程序代码中编程式地设置,或者声明式地设置。

1.编程式的事务划分

在非托管环境中,JDBC API被用来给事务范围做标记。通过在JDBC Connection中调用setAutoCommit(false)启动事务,并通过调用commit()终止它。可以在任何时候通过调用rollback()强制立即回滚。

在一个于几个数据库中操作数据的系统中,特定的工作单元涉及对不止一个资源的访问。既然如此,你就无法单独通过JDBC实现原子性。你需要可以在系统事务(system transaction)中处理几个资源的事务管理器(transaction manager)。这样的事务处理系统公开了与开发人员进行交互的Java Transaction API(JTA)。JTA中的主API是UserTransaction接口,包含begin()和commit()系统事务的方法。

此外,Hibernate应用程序中的编程式事务管理通过Hibernate Transaction接口公开给应用程序开发人员。你并没有被强制使用这个API——Hibernate也让你直接启动和终止JDBC事务,但不鼓励这种用法,因为它把代码绑定到了直接的JDBC。在Java EE环境中(或者如果你把它和Java SE应用程序一起安装了),就可以使用JTA兼容的事务管理器,因此你应该调用JTA UserTransaction接口来编程式地启动和终止事务。然而,就像你可能已经猜到的,Hibernate Transaction接口在JTA的顶层也有效。我们将介绍所有这些方法,并更详细地讨论可移植性的关注点。

利用Java Persistence的编程式事务划分,还必须在Java EE应用程序服务器的内部和外部使用。在应用程序服务器外部,利用简单的Java SE,处理本地资源事务;这是EntityTransaction接口的作用——你已经在前面几章中见过它。在应用程序服务器内部,调用JTA UserTran- saction接口来启动和终止事务。

让我们概括一下这些接口,以及什么时候使用它们:

l    java.sql.Connection——利用setAutoCommit(false)、commit()和rollback()进行简单的JDBC事务划分。它可以但不应该被用在Hibernate应用程序中,因为它把应用程序绑定到了一个简单的JDBC环境。

l    org.hibernate.Transaction——Hibernate应用程序中统一的事务划分。它适用于非托管的简单JDBC环境,也适用于以JTA作为底层系统事务服务的应用程序服务器。但是,它最主要的好处在于与持久化上下文管理的紧密整合——例如,你提交时Session被自动清除。持久化上下文也可以拥有这个事务的范围(对会话有用,请见第11章)。如果你无法具备JTA兼容的事务服务,就使用Java SE中的这个API。

l    javax.transaction.UserTransaction——Java中编程式事务控制的标准接口,它是JTA的一部分。每当你具备JTA兼容的事务服务,并想编程式地控制事务时,它就应该成为你的首选。

l    javax.persistence.EntityTransaction——在使用Java Persistence的Java SE应用程序中,编程式事务控制的标准接口。

另一方面,声明式事务划分不需要额外的代码;并且从定义上来说,它解决了可移植性的问题。

2.声明式事务划分

在应用程序中,当你希望在一个事务内部进行工作的时候要进行声明(例如,在方法中使用注解)。然后处理这个关注点就是应用部署程序和运行时环境的责任了。Java中提供声明式事务服务的标准容器是EJB容器,这项服务也称作容器托管事务。我们将再次编写EJB会话bean,介绍Hibernate和Java Persistence如何从这项服务中受益。

在你决定一种特定的API之前,或者为了声明式事务划分,让我们一步步来探讨这些选项。首先,假设你正要在一个简单的Java SE应用程序(一个客户端/服务器端Web应用程序、桌面应用程序或者任何两层系统)中使用原生的Hibernate。之后,要把代码重构到一个托管的Java EE环境下运行(并且首先看看如何避免这种重构)。我们也顺便讨论Java Persistence。

10.1.2  Hibernate应用程序中的事务

想象你正在编写一个必须在简单的Java中运行的Hibernate应用程序;没有容器、没有托管的数据库资源可以使用。

1.Java SE中的编程式事务

配置Hibernate为你创建一个JDBC连接池,就像在2.1.3节中所做的那样。如果你正在利用Transaction API编写Java SE Hibernate应用程序,除了连接池之外,不需要其他的配置设置:

l    hibernate.transaction.factory_class选项默认为org.hibernate.transaction. JDBCTransactionFactory,这是Java SE中Transaction API以及直接的JDBC的正确工厂。

l    可以用自己的TransactionFactory实现扩展和定制Transaction接口。这几乎没有必要,但是有一些值得关注的使用案例。例如,每当启动事务时,如果你必须编写审计日志,就可以把这个日志添加到一个定制的Transaction实现。

Hibernate为你正要使用的每个Session获得一个JDBC连接:

Hibernate Session是延迟的。这是件好事——意味着它不消费任何资源,除非绝对需要。只有当数据库事务启动时,才从连接池中获得JDBC Connection。对beginTransaction()的调用在新的JDBC Connection中转变成setAutoCommit(false)。现在Session被绑定到了这个数据库连接,并且所有的SQL语句(在这个例子中,终止拍卖所需要的所有SQL)都在该连接上发送。所有的数据库语句都在同一个数据库事务内部执行。(假设concludeAuction()方法调用指定的Session来访问数据库。)

我们已经谈到过迟写行为,因此你知道当Session的持久化上下文被清除时,大批量的SQL语句被尽可能迟地执行。默认情况下,这发生在当你在Transaction上调用commit()的时候。提交事务(或者把它回滚)之后,数据库连接被释放,并且从Session中解除绑定。用相同的Session启动一个新的事务,从连接池中获取另一个连接。

关闭Session释放出所有其他的资源(例如,持久化上下文);所有托管的持久化实例现在都被认为是脱管的。

常见问题 回滚只读事务更快吗?如果事务中的代码读取数据但没有修改它,你应该回滚事务而不是提交它吗?这样更快吗?很显然,有些开发人员发现这在某些特殊环境下会更快,并且这种信念已经传播到了整个社区。我们用更为普及的数据库系统进行过测试,发现并没有区别。我们也没有发现显示性能差别的真实数据的任何来源。对于数据库系统为什么应该有一个并非最佳的实现——为什么它不应该内部使用最快的事务清除算法,也没有很好的解释。始终提交事务,如果提交失败就回滚。话虽这么说,但SQL标准还是包括了一个SET TRANSACTION READ ONLY(设置事务只读)的语句。Hibernate不支持启用这个设置的API,虽然你可以实现自己定制的Transaction和TransactionFactory来添加这个操作。我们建议你先查一下数据库是否支持这一点,以及可能的性能受益有什么,如果有,就可以了。

我们现在要讨论异常处理。

2.处理异常

如果上一个例子中介绍过的concludeAuction()(或者提交期间持久化上下文的清除)抛出异常,你就必须调用tx.rollback()强制事务回滚。它会立即回滚事务,因此你发送到数据库的SQL操作对性能没有任何影响。

这似乎很简单,虽然或许你可能已经看到,每当你想要访问数据库的时候,捕捉RuntimeException都不会产生好代码。

说明 异常的历史——异常以及异常应该如何处理,始终是Java开发人员之间争论的热点。Hibernate也有一些值得瞩目的历史,这并不奇怪。在Hibernate 3.x之前,Hibernate抛出的所有异常均为checked exception(已检查异常),因此每个Hibernate API都强制开发人员捕捉和处理异常。这个策略受到JDBC影响,它也只抛出checked exception。但是很快就清楚了,这样没有意义,因为Hibernate抛出的所有异常都是致命的。在许多情况下,开发人员在这种情形之下所能做的就是清除,显示错误消息,并退出应用程序。因此,从Hibernate 3.x开始,Hibernate抛出的所有异常都是unchecked(未检查)的RuntimeException的子类型,它通常在应用程序中单个的位置进行处理。这也使得所有Hibernate模板或者包装API都变得没用了。

首先,即使我们承认你不用许多(或者成百个)try/catch块编写应用程序代码,我们介绍过的例子也是不完整的。这是一个Hibernate工作单元标准惯用语的例子,带有包含真实的异常处理的数据库事务:

任何Hibernate操作,包括清除持久化上下文,都可能抛出RuntimeException。甚至回滚事务也可能抛出异常!你想要捕捉这个异常并记录到日志中;否则,导致回滚的原始异常就被淹没了。

这个例子中可选的方法调用是setTimeout(),它获得允许事务运行的秒数。然而,在Java SE环境中并没有真正被监测的事务。如果在应用程序服务器外部运行这段代码(也就是说,没有事务管理器),Hibernate所能做的最好的就是给驱动器等待PreparedStatement执行的时间设置秒数(Hibernate专用的预编译语句)。如果超出限制,就抛出SQLException。

你不想用这个例子作为自己应用程序中的一个模板,因为你应该通过一般的基础结构代码来隐藏异常处理。例如,可以给RuntimeException编写单个错误处理程序,知道什么时候以及如何回滚事务。对于开启和关闭Session也一样。稍后第11章会用更真实的例子讨论它,16.1.3节也会再次讨论它。

Hibernate抛出类型(typed)异常,即帮助你辨别错误的RuntimeException的所有子类型:

l    最常见的HibernateException是个一般的错误。你必须检查异常消息,或者在通过异常中调用getCause()找出更多原因。

l    JDBCException是被Hibernate的内部JDBC层抛出的任何异常。这种异常总是由一个特定的SQL语句产生,可以用getSQL()获得这个引起麻烦的语句。JDBC连接(实际上是JDBC驱动器)抛出的内部异常可以通过getSQLException()或者getCause()获得,并且通过getErrorCode()可以得到特定于数据库和特定于供应商的错误代码。

l    Hibernate包括JDBCException的子类型和一个内部转换器,该转换器试图把数据库驱动抛出器的特定于供应商的错误代码变成一些更有意义的东西。内建的转换器可以给Hibernate支持的最重要的数据库方言生成JDBCConnectionException、SQLGrammarException、LockAquisitionException、DataException和ConstraintViolationException。可以对数据库操作或者增强方言,或者插入SQLExceptionConverterFactory定制这种变换。

l    Hibernate抛出的其他RuntimeException也应该终止事务。应该始终确保捕捉RuntimeException,无论你计划利用任何细粒度的异常处理策略去做什么。

你现在知道了应该捕捉什么异常,以及它们什么时候会出现。但是你心里可能有一个问题:捕捉到异常之后应该怎么办?

Hibernate抛出的所有异常都是致命的。这意味着你必须回滚数据库事务,并关闭当前的Session。不允许你继续使用抛出异常的Session。

通常,你还必须在关闭异常后面的Session之后退出应用程序,虽然有一些异常(例如StaleObjectStateException)一般导致在新Session中一个新的尝试(可能在再次与应用程序的用户交互之后)。由于这些与会话以及并发控制密切相关,我们将在稍后讨论它们。

常见问题 可以把异常用于验证吗?有些开发人员一旦知道Hibernate可以抛出许多细粒度的异常类型后就很兴奋。这样可能把你引向歧途。例如,你可能试图捕捉ConstrainViolationException用于验证。如果一个特定的操作抛出异常,为什么不显示一条(根据错误代码和文本而定制的)失败消息给应用程序的用户,让他们纠正错误呢?这个策略有两个明显的缺点。第一,依靠数据库抛出unchecked值来查看问题,对于可伸缩的应用程序而言,这并不是一种好策略。你想要至少在应用层实现一些数据完整性的验证。第二,所有的异常对于当前的工作单元而言都是致命的。然而,这并不是应用程序的用户对验证错误的理解——他们希望仍然处在工作单元内部。围绕这种不匹配编写的代码很笨拙也很困难。我们的建议是,使用细粒度的异常类型显示更好看的(致命)错误消息。这么做在开发期间有帮助(理想情况下,不应该有致命的异常出现在产品中),也帮助任何客户支持必须迅速做出决定的工程师,如果它是个应用程序错误(违背约束,执行了错误的SQL)或者如果数据库系统正在加载(无法获得锁)。

利用Hibernate Transaction接口在Java SE中编程式的事务划分,使代码保持可移植。它也可以在托管环境内部运行,当事务管理器处理数据库资源的时候。

3.使用JTA的编程式事务

能与Java EE兼容的托管运行时环境,能够为你管理资源。在大多数情况下,被管理的资源都是数据库连接,但是任何带有适配器(adaptor)的资源都可以与Java EE系统整合(如消息或者遗留系统)。在那些资源上的编程式事务划分,如果它们是事务的,就被统一并公开给使用JTA的开发人员;javax.transaction.UserTransaction是启动和终止事务的主要接口。

常见的托管运行时环境是Java EE应用程序服务器。当然,应用程序服务器提供更多的服务,而不仅仅是资源管理。许多Java EE服务是模块化的——安装应用程序服务器不是获得它们的唯一方法。如果你只需要托管的资源,可以获得一个独立的JTA提供程序。开源独立的JTA提供程序包括JBoss Transactions(http://www.jboss.com/products/transactions)、ObjectWeb JOTM(http://jotm.objectweb.org)和其他的。可以和Hibernate应用程序一起安装这样一个JTA服务(例如在Tomcat中)。它将替你管理数据库连接池,给事务划分提供JTA接口,并通过JNDI注册提供托管的数据库连接。

下列是使用JTA的托管资源的好处,以及使用这个Java EE服务的理由:

l    事务管理服务可以统一所有的资源,无论什么类型,并用单个标准的API把事务控制公开给你。这意味着你可以替换Hibernate的Transaction API,并直接在任何地方使用JTA。然后,在(或者用)JTA兼容的运行时环境中安装应用程序就是应用程序部署人员的责任了。这个策略把可移植性关注点归属的位置转移了;应用程序依赖标准的Java EE接口,并且运行时环境必须提供实现。

l    Java EE事务管理器可以在单个事务中获取多个资源。如果你使用几个数据库(或者不止一个资源),就可能想要一个两阶段提交(two-phase commit)协议来保证跨资源范围的事务原子性。在这样的场景中,Hibernate通过配置几个SessionFactory(每个数据库一个),他们的Session获得所有参与同一个系统事务的托管数据库连接。

l    与简单的JDBC连接池相比,JTA实现的质量通常更高。应用程序服务器和作为应用程序服务器模块的独立JTA提供程序,通常在包含大量事务的高端系统中已经进行过更多的测试。

l    JTA提供程序在运行时不增加不必要的过载(一种常见的错误概念)。简单案例(单个JDBC数据库)的处理与使用简单的JDBC事务一样有效。比起与简单的JDBC一起使用的随机连接池库,JTA服务背后托管的连接池可能是更好的软件。

假设你不喜欢JTA,并想要继续使用Hibernate Transaction API来保证代码在Java EE中和使用托管Java EE服务时可以运行,而不用改变任何代码。为了部署前面的代码实例(它们全部在Java EE应用程序服务器上调用Hibernate Transaction API),你需要把Hibernate配置转换到JTA:

l    hibernate.transaction.factory_class选项必须设置为org.hibernate.transaction. JTATransactionFactory。

l    Hibernate需要知道你正在哪个JTA实现中部署,这出于两个原因:第一,不同的实现可能公开JTA UserTransaction,Hibernate现在必须以不同的名称内部调用它。第二,Hibernate必须钩进JTA事务管理器的同步过程来处理它的高速缓存。你必须设置hibernate. transaction.manager_lookup_class选项来配置这两项:例如,配置为org.hibernate. transaction.JBossTransactionManagerLookup。在类中查找最常见的JTA实现,并且应用程序服务器中附有Hibernate(需要时可以定制)。在Javadoc中检查是否有包。

l    Hibernate不再负责管理JDBC连接池;它从运行时容器获得托管的数据库连接。这些连接被JTA提供程序通过JNDI这个全局的注册而被公开。你必须在JNDI上使用正确的名称对数据库资源配置Hibernate,就像在2.4.1节中所做的那样。

现在,你之前直接在JDBC顶层给Java SE编写的同一段代码,在包含托管数据源的JTA环境中也有效:

然而,数据库连接处理稍微不同。Hibernate给你正在使用的每个Session获得一个托管的数据库连接,并且还是努力尽可能地延迟。没有JTA,Hibernate将从一开始就停在一个特定的数据库连接上,直接事务终止。有了JTA配置,Hibernate甚至更为积极:获得一个连接,并只用于单个SQL语句,然后立即被返回到托管的连接池。应用程序服务器保证当另一个SQL语句再次需要连接时,它将在同一个事务期间分发出同一个连接。这个积极的连接—释放模式是Hibernate的内部行为,对于应用程序以及如何编写代码,都没有任何影响。(因而,这个代码实例每一行都与原来的一样。)

JTA系统支持全局的事务超时;它可以监控事务。因此,现在setTimeout()控制全局的JTA超时设置——相当于调用UserTransaction.setTransactionTimeout()。

Hibernate Transaction API用Hibernate配置一个简单的变化来保证可移植性。如果你想要把这个责任转移到应用部署程序上,就应该针对标准的JTA接口编写代码。为使下列代码更值得关注,你还将在同一个系统事务内部使用两个数据库(两个SessionFactory):

(注意,这个代码片段可以抛出一些其他的checked exception,就像从JNDI查找中抛出的NamingException。你需要对这些做相应的处理。)

首先,必须从JNDI注册上获得JTA UserTransaction上的句柄。然后,开启和关闭事务,并且所有的Hibernate Session所用的(容器提供)数据库连接都在这个事务中被自动获取。即使你没有使用Transaction API,也仍然应该给JTA和环境配置hibernate.transaction.factory_class和hibernate.transaction.manager_lookup_class,以便Hibernate可以与事务系统内部进行交互。

利用默认的设置,手工flush()每个Session,使它与数据库同步(来执行所有SQL DML)也是你的责任了。Hibernate Transaction API以前自动为你完成。你还必须手工关闭所有的Session。另一方面,可以启用hibernate.transaction.flush_before_completion和(或)hibernate.transaction.auto_close_session配置选项,并再次让Hibernate替你负责这项工作——然后,清除和关闭就成为事务管理器内部同步过程的一部分,并且发生在JTA事务终止之前(相应地,或者之后)。启用这两项设置的代码可以简化为如下:

session1和session2持久化上下文现在在UserTransaction的提交期间被自动清除,并且两者都在事务完成之后关闭。

建议尽可能地直接使用JTA。你应该始终努力把可移植性的责任转移到应用程序之外,如果可以,要求部署在一个提供JTA的环境中。

编程式的事务划分需要针对事务划分接口而编写的应用程序代码。避免任何不可移植的代码传播到整个应用程序的一种更好的方法是,声明式(declarative)事务划分。

4.容器托管事务

声明式事务划分意味着容器替你负责这个关注点。你声明想要代码在一个事务中是否参与以及如何参与。通过应用程序部署人员,提供支持声明式事务划分容器的责任再次落到了它所属的地方。

CMT是Java EE尤其JEB的标准特性,接下来要介绍的代码是基于EJB 3.0会话bean的(只适用于Java EE),你利用注解定义事务范围。注意,如果你必须使用更早版本的EJB 2.1会话bean,实际的数据访问代码并没有改变;然而,必须用XML编写EJB部署描述符来创建事务装配——这在EJB 3.0中是可选的。

(独立的JTA实现不提供容器托管和声明式事务。但是JBoss应用程序服务器可以当作模块服务器使用,包含最少的模块,如有必要,它可以只提供JTA和EJB 3.0容器。)

假设EJB 3.0会话bean实现了一个终止拍卖的动作。你之前用编程式的JTA事务划分编写的代码被移到了一个无状态会话bean里面:

容器注意到了你的TransactionAttribute声明,并把它应用到endAuction()方法。如果调用方法时没有系统事务在运行,就会启动一个新的事务(这是REQUIRED)。一旦方法返回,并且如果调用这个方法(不是任何其他方法)时启动了事务,这个事务就提交。如果方法内部的代码抛出RuntimeException,系统事务就自动回滚。

为了方便举例,我们再给两个数据库引入两个SessionFactory。它们可以通过JNDI查找(Hibernate可以在启动时绑定它们)或者从HibernateUtil的增强版中进行分配。这两者都获得通过相同的容器托管事务而获取的数据库连接。如果容器的事务系统和资源支持它,你就再次获得一个两阶段提交协议,确保跨数据库的事务原子性。

必须用Hibernate设置一些配置选项来启用CMT:

l    hibernate.transaction.factory_class选项必须被设置为org.hibernate. transaction.CMTTransactionFactory。

l    你需要给应用程序服务器设置hibernate.transaction.manager_lookup_class为正确的查找类。

还要注意,所有EJB会话bean都默认为CMT,因此如果你想要禁用CMT并在任何会话bean方法中直接调用JTA USerTransaction,就用@TransactionManagement(Transaction- ManagementType.BEAN)注解EJB类。然后你就是在使用bean托管事务(bean-managed transaction,BMT)了。即使它可能适用于大多数的应用程序服务器,但是Java EE规范还是不允许在单个bean中混合CMT和BMT。

CMT代码看起来已经比编程式的事务划分好多了。如果配置Hibernate来使用CMT,它就知道应该自动清除和关闭参与系统事务的Session。此外,你将很快改进这段代码,甚至移除打开Hibernate Session的那两行。

来看看Java Persistence应用程序中的事务处理。

10.1.3  使用Java Persistence的事务

使用Java Persistence也要进行设计抉择:是在应用程序代码中编程式地事务划分,还是由运行时容器自动处理声明式事务划分。先通过简单的Java SE探讨第一种方法,然后用JTA和EJB组件重复例子。

描述本地资源(resource-local)的事务应用到由应用程序(编程式地)控制的、且不参与全局系统事务的所有事务。它们直接变为你正在处理的资源的本地事务系统。由于你正在使用JDBC数据库,这意味着本地资源的事务变成了JDBC数据库事务。

JPA中本地资源的事务通过EntityTransaction API控制。这个接口不是为可移植性而存在,而是用来启用Java Persistence的特定特性——例如,当提交事务时,底层持久化上下文的清除。

你已经多次在Java SE中见过Java Persistence的标准习惯用语。以下代码仍然带有异常处理:

这个模式接近于它的Hibernate等价物,隐含着相同的含义:必须手工启动和终止数据库事务,并且必须保证应用程序托管的EntityManager在finaly块中关闭。(虽然我们经常介绍不处理异常或者包在try/catch块中的代码示例,但这不是可选的。)

JPA抛出的异常是RuntimeException的子类型。任何异常都使得当前的持久化上下文无效,并且一旦抛出异常,就不允许你继续使用EntityManager。因此,我们对Hibernate异常处理所讨论的所有策略也适用于Java Persistence异常处理。此外,下列规则也适用:

l    由EntityManager接口的任何方法抛出的任何异常,都会触发当前事务的自动回滚。

l    由javax.persistence.Query接口的任何方法抛出的任何异常,都会触发当前事务的自动回滚,除了NoResultException和NonUniqueResultException之外。因此,前一个捕捉所有异常的代码示例也为这些异常执行回滚。

注意,JPA不提供细粒度的SQL异常类型。最常见的异常是javax.peristence. PersistenceException。被抛出的所有其他异常都是PersistenceException的子类型,除了NoResultException和NonUniqueResultException之外,应该把它们全部都当作是致命的。然而,可以在JPA抛出的任何异常中调用getCause(),并找出被包装的原生Hibernate异常,包括细粒度的SQL异常类型。

如果你在应用程序服务器内部或者至少提供JTA(对于Hibernate请见我们前面的讨论)的环境中使用Java Persistence,就对编程式的事务划分调用JTA接口。EntityTransaction接口只对本地资源的事务可用。

1.使用Java Persistence的JTA事务

如果Java Persistence代码是在一个可用的JTA环境中部署,并且你想要使用JTA系统事务,就需要调用JTA UserTransaction接口来编程式地控制事务范围:

EntityManager的持久化上下文被界定到JTA事务。被这个EntityManager清除的所有SQL语句,都在通过事务而获取的数据库连接中的一个JTA事务内部执行。当JTA事务提交时,持久化上下文被自动清除和关闭。可以用几个EntityManager在同一个系统事务中访问几个数据库,就像在原生的Hibernate应用程序中使用几个Session一样。

注意持久化上下文的范围改变了!现在它被界定到了JTA事务,并且一旦提交事务,事务期间原来处于持久化状态的所有对象就立即被视为脱管。

异常处理的规则相当于那些用于本地资源的事务的规则。如果在EJB中使用JTA,别忘了在类上设置@TransactionManagement(TransactionManagementType.BEAN)来启用BMT。

你不会经常通过JTA使用Java Persistence,也没有可用的EJB容器。如果你没有部署一个独立的JTA实现,Java EE 5.0应用程序服务器就将提供这两样东西。你不用编程式的事务划分,而是可能使用EJB的声明特性。

2.Java Persistence和CMT

让我们从前面只用Hibernate的示例中,把ManageAuction EJB会话bean重构到Java Persistence接口。你还让容器注入EntityManager:

在concludeAuction()和billAuction()方法内部发生的一切与这个例子仍不相关;假设它们需要用EntityManager来访问数据库。用于endAuction()方法的TransactionAttribute要求所有的数据库访问都发生在一个事务内部。如果endAuction()调用时,没有系统事务是活动的,就会给这个方法启动一个新事务。如果方法返回,且如果事务是为这个方法启动的,它就被提交。每个EntityManager都有一个跨越事务范围、并且当事务结束时被自动清除的持久化上下文。如果方法被调用时没有活动的事务,持久化上下文就有与endAuction()方法相同的范围。

这两个持久化单元都被配置为在JTA上部署,因此两个托管的数据库连接(每个数据库一个),都在同一个事务内部获得,原子性受到应用程序服务器的事务管理器的保护。

你声明endAuction()方法可以抛出AuctionNotValidException。这是你编写的一个定制异常;在终止拍卖之前,检查是否一切都准确无误(已经到达拍卖终止时间、有一个出价等)。这是个checked exception,是java.lang.Exception的子类型。EJB容器把它当成应用程序异常(application exception)处理,如果EJB方法抛出这个异常,则不会触发任何动作。然而,容器认识系统异常(system exception),它默认为所有可能被EJB方法抛出的未检查RuntimeException。由EJB方法抛出的系统异常强制系统事务的自动回滚。

换句话说,你不需要从Java Persistence操作中捕捉和重新抛出任何系统异常——让容器来处理它们。如果抛出应用程序异常,那么对于如何回滚事务,你有两种选择:第一,可以捕捉它,并手工调用JTA UserTransaction,设置它回滚。或者把@ApplicationException(rollback=true)注解添加到AuctionNotValidException的类——然后容器就知道,你希望每当EJB方法抛出这个应用程序异常时就自动回滚。

现在,你准备在应用程序服务器的内部和外部、用或者不用JTA,以及在EJB和容器管理事务的组合中使用Java Persistence和Hibernate。我们已经讨论了事务原子性的(几乎)所有方面。对于并发运行的事务之间的隔离性,你自然可能仍存疑虑。

查看所有评论(0)条】

最近评论



正在载入评论列表...
热点评论