12.2. 数据库事务声明
数 据库(或者系统)事务的声明总是必须的。在数据库事务之外,就无法和数据库通讯(这可能会让那些习惯于自动提交事务模式的开发人员感到迷惑)。永远使用清 晰的事务声明,即使只读操作也是如此。进行 显式的事务声明并不总是需要的,这取决于你的事务隔离级别和数据库的能力,但不管怎么说,声明事务总归有益无害。当然,一个单独的数据库事务总是比很多琐 碎的事务性能更好,即时对读数据而言也是一样。
一 个 Hibernate 应用程序可以运行在非托管环境中(也就是独立运行的应用程序,简单 Web 应用程序,或者Swing图形桌面应用程序),也可以运行在托管的 J2EE 环境中。在一个非托管环境中,Hibernate 通常自己负责管理数据库连接池。应用程序开发人员必须手工设置事务声明,换句话说,就是手工启 动,提交,或者回滚数据库事务。一个托管的环境通常提供了容器管理事务(CMT),例如事务装配通过可声明的方式定义在 EJB session beans 的部署描述符中。可编程式事务声明不再需要,即使是
Session
的同步也可以自动完成。
让持久层具备可移植性是人们的理想,这种移植发生在非托管的本地资源环境,与依赖 JTA 但是使用 BMT 而非 CMT 的系统之间。在两种情况下你都可以使用编程式的事务管理。Hibernate 提供了一套称为
Transaction
的封装 API, 用来把你的部署环境中的本地事务管理系统转换到 Hibernate 事务上。这个 API 是可选的,但是我们强烈推荐你使用,除非你用 CMT session bean。
通常情况下,结束
Session
包含了四个不同的阶段:
同步 session(flush,刷出到磁盘)
提交事务
关闭 session
处理异常
session 的同步(flush,刷出)前面已经讨论过了,我们现在进一步考察在托管和非托管环境下的事务声明和异常处理。
12.2.1. 非托管环境
如果 Hibernat 持久层运行在一个非托管环境中,数据库连接通常由 Hibernate 的简单(即非 DataSource)连接池机制 来处理。session/transaction 处理方式如下所示:
// Non-managed environment idiom
Session sess = factory.openSession();
Transaction tx = null;
try {
tx = sess.beginTransaction();
// do some work
...
tx.commit();
}
catch (RuntimeException e) {
if (tx != null) tx.rollback();
throw e; // or display error message
}
finally {
sess.close();
}
你不需要显式
flush()
Session
— 对 commit()
的调用会自动触发 session 的同步(取决于 session 的 第 10.10 节 “Session 刷出(flush)”)。调用 close()
标志 session 的结束。close()
方法重要的暗示是,session
释放了 JDBC 连接。这段 Java 代码在非托管环境下和 JTA 环境下都可以运行。
更加灵活的方案是 Hibernate 内置的 "current session" 上下文管理,前文已经讲过:
// Non-managed environment idiom with getCurrentSession()
try {
factory.getCurrentSession().beginTransaction();
// do some work
...
factory.getCurrentSession().getTransaction().commit();
}
catch (RuntimeException e) {
factory.getCurrentSession().getTransaction().rollback();
throw e; // or display error message
}
你很可能从未在一个通常的应用程序的业务代码中见过这样的代码片断:致命的(系统)异常应该总是 在应用程序“顶层”被捕获。换句话说,执行 Hibernate 调用的代码(在持久层)和处理
RuntimeException
异常的代码(通常只能清理和退出应用程序)应该在不同 的应用程序逻辑层。Hibernate 的当前上下文管理可以极大地简化这一设计,你所有的一切就是 SessionFactory
。异常处理将在本章稍后进行讨论。
请注意,你应该选择
org.hibernate.transaction.JDBCTransactionFactory
(这是默认选项),对第二个例子来说,hibernate.current_session_context_class
应该是 "thread"
。
12.2.2. 使用 JTA
如 果你的持久层运行在一个应用服务器中(例如,在 EJB session beans 的后面),Hibernate 获取的每个数据源连接将自动成为全局 JTA 事务的一部分。你可以安装一个独立的 JTA 实现,使用它而不使用 EJB。Hibernate 提供了两种策略进行 JTA 集成。
如果你使用 bean 管理事务(BMT),可以通过使用 Hibernate 的
Transaction
API 来告诉应用服务器启动和结束 BMT 事务。因此,事务管理代码和在非托管环境下是一样的。
// BMT idiom
Session sess = factory.openSession();
Transaction tx = null;
try {
tx = sess.beginTransaction();
// do some work
...
tx.commit();
}
catch (RuntimeException e) {
if (tx != null) tx.rollback();
throw e; // or display error message
}
finally {
sess.close();
}
如果你希望使用与事务绑定的
Session
,也就是使用 getCurrentSession()
来简化上下文管理,你将不得不直接使用 JTA UserTransaction
API。
// BMT idiom with getCurrentSession()
try {
UserTransaction tx = (UserTransaction)new InitialContext()
.lookup("java:comp/UserTransaction");
tx.begin();
// Do some work on Session bound to transaction
factory.getCurrentSession().load(...);
factory.getCurrentSession().persist(...);
tx.commit();
}
catch (RuntimeException e) {
tx.rollback();
throw e; // or display error message
}
在 CMT 方式下,事务声明是在 session bean 的部署描述符中,而不需要编程。因此,代码被简化为:
// CMT idiom
Session sess = factory.getCurrentSession();
// do some work
...
在 CMT/EJB 中甚至会自动 rollback,因为假若有未捕获的
RuntimeException
从 session bean 方法中抛出,这就会通知容器把全局事务回滚。这就意味着,在 BMT 或者 CMT 中,你根本就不需要使用 Hibernate Transaction
API,你自动得到了绑定到事务的“当前” Session。
注意,当你配置 Hibernate 的 transaction factory 的时候,在直接使用 JTA 的时候(BMT),你应该选择
org.hibernate.transaction.JTATransactionFactory
,在 CMT session bean 中选择 org.hibernate.transaction.CMTTransactionFactory
。记得也要设置 hibernate.transaction.manager_lookup_class
。还有,确认你的 hibernate.current_session_context_class
未设置(为了向下兼容),或者设置为 "jta"
。
getCurrentSession()
在 JTA 环境中有一个弊端。对 after_statement
连接释放方式有一个警告,这是被默认使用的。因为 JTA 规范的一个很愚蠢的限制,Hibernate 不可能自动清理任何未关闭的 ScrollableResults
或者Iterator
,它们是由 scroll()
或 iterate()
产生的。你 must 通过在 finally
块中,显式调用 ScrollableResults.close()
或者 Hibernate.close(Iterator)
方法来释放底层数据库游标。(当然,大部分程序完全可以很容易的避免在 JTA 或 CMT 代码中出现 scroll()
或 iterate()
。)
12.2.3. 异常处理
如果
Session
抛出异常(包括任何 SQLException
),你应该立即回滚数据库事务,调用 Session.close()
,丢弃该 Session
实例。Session
的某些方法可能会导致 session 处于不一致的状态。所有由 Hibernate 抛出的异常都视为不可以恢复的。确保在 finally
代码块中调用 close()
方法,以关闭掉 Session
。
HibernateException
是一个非检查期异常(这不同于 Hibernate 老的版本),它封装了 Hibernate 持久层可能出现的大多数错误。我们的观点是,不应该强迫应用程序开发人员 在底层捕获无法恢复的异常。在大多数软件系统中,非检查期异常和致命异常都是在相应方法调用 的堆栈的顶层被处理的(也就是说,在软件上面的逻辑层),并且提供一个错误信息给应用软件的用户 (或者采取其他某些相应的操作)。请注意,Hibernate 也有可能抛出其他并不属于 HibernateException
的非检查期异常。这些异常同样也是无法恢复的,应该 采取某些相应的操作去处理。
在和数据库进行交互时,Hibernate 把捕获的
SQLException
封装为 Hibernate 的 JDBCException
。事实上,Hibernate 尝试把异常转换为更有实际含义的 JDBCException
异常的子类。底层的 SQLException
可以通过 JDBCException.getCause()
来得到。Hibernate 通过使用关联到 SessionFactory
上的 SQLExceptionConverter
来把 SQLException
转换为一个对应的 JDBCException
异常的子类。默认情况下,SQLExceptionConverter
可以通过配置 dialect 选项指定;此外,也可以使用用户自定义的实现类(参考 javadocs SQLExceptionConverterFactory
类来了解详情)。标准的 JDBCException
子类型是:
JDBCConnectionException
:指明底层的 JDBC 通讯出现错误。SQLGrammarException
:指明发送的 SQL 语句的语法或者格式错误。ConstraintViolationException
:指明某种类型的约束违例错误LockAcquisitionException
:指明了在执行请求操作时,获取所需的锁级别时出现的错误。GenericJDBCException
:不属于任何其他种类的原生异常。
12.2.4. 事务超时
EJB 这样的托管环境有一项极为重要的特性,而它从未在非托管环境中提供过,那就是事务超时。在出现错误的事务行为的时候,超时可以确保不会无限挂起资源、对用 户没有交代。在托管(JTA)环境之外,Hibernate 无法完全提供这一功能。但是,Hiberante 至少可以控制数据访问,确保数据库级别的死锁,和返回巨大结果集的查询被限定在一个规定的时间内。在托管环境中,Hibernate 会把事务超时转交给 JTA。这一功能通过 Hibernate
Transaction
对象进行抽象。
Session sess = factory.openSession();
try {
//set transaction timeout to 3 seconds
sess.getTransaction().setTimeout(3);
sess.getTransaction().begin();
// do some work
...
sess.getTransaction().commit()
}
catch (RuntimeException e) {
sess.getTransaction().rollback();
throw e; // or display error message
}
finally {
sess.close();
}
注意
setTimeout()
不应该在 CMT bean 中调用,此时事务超时值应该是被声明式定义的。