11.2. 数据库事务声明

Database (or system) transaction boundaries are always necessary. No communication with the database can occur outside of a database transaction (this seems to confuse many developers who are used to the auto-commit mode). Always use clear transaction boundaries, even for read-only operations. Depending on your isolation level and database capabilities this might not be required but there is no downside if you always demarcate transactions explicitly. Certainly, a single database transaction is going to perform better than many small transactions, even for reading data.

一个Hibernate应用程序可以运行在非托管环境中(也就是独立运行的应用程序,简单Web应用程序, 或者Swing图形桌面应用程序),也可以运行在托管的J2EE环境中。在一个非托管环境中,Hibernate 通常自己负责管理数据库连接池。应用程序开发人员必须手工设置事务声明,换句话说,就是手工启 动,提交,或者回滚数据库事务。一个托管的环境通常提供了容器管理事务(CMT),例如事务装配通过可声 明的方式定义在EJB session beans的部署描述符中。可编程式事务声明不再需要,即使是 Session 的同步也可以自动完成。

However, it is often desirable to keep your persistence layer portable between non-managed resource-local environments, and systems that can rely on JTA but use BMT instead of CMT. In both cases you'd use programmatic transaction demarcation. Hibernate offers a wrapper API called Transaction that translates into the native transaction system of your deployment environment. This API is actually optional, but we strongly encourage its use unless you are in a CMT session bean.

通常情况下,结束 Session 包含了四个不同的阶段:

session的同步(flush,刷出)前面已经讨论过了,我们现在进一步考察在托管和非托管环境下的事务声明和异常处理。

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

You don't have to flush() the Session explicitly - the call to commit() automatically triggers the synchronization (depending upon the FlushMode for the session. A call to close() marks the end of a session. The main implication of close() is that the JDBC connection will be relinquished by the session. This Java code is portable and runs in both non-managed and JTA environments.

更加灵活的方案是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"

11.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 UserTransactionAPI。

// 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
}

With CMT, transaction demarcation is done in session bean deployment descriptors, not programmatically, hence, the code is reduced to:

// CMT idiom
 Session sess = factory.getCurrentSession();

 // do some work
 ...

在CMT/EJB中甚至会自动rollback,因为假若有未捕获的RuntimeException从session bean方法中抛出,这就会通知容器把全局事务回滚。这就意味着,在BMT或者CMT中,你根本就不需要使用Hibernate Transaction API ,你自动得到了绑定到事务的“当前”Session。

Note that you should choose org.hibernate.transaction.JTATransactionFactory if you use JTA directly (BMT), and org.hibernate.transaction.CMTTransactionFactory in a CMT session bean, when you configure Hibernate's transaction factory. Remember to also set hibernate.transaction.manager_lookup_class. Furthermore, make sure that your hibernate.current_session_context_class is either unset (backwards compatibility), or set to "jta".

The getCurrentSession() operation has one downside in a JTA environment. There is one caveat to the use of after_statement connection release mode, which is then used by default. Due to a silly limitation of the JTA spec, it is not possible for Hibernate to automatically clean up any unclosed ScrollableResults or Iterator instances returned by scroll() or iterate(). You must release the underlying database cursor by calling ScrollableResults.close() or Hibernate.close(Iterator) explicitly from a finally block. (Of course, most applications can easily avoid using scroll() or iterate() at all from the JTA or CMT code.)

11.2.3. 异常处理

如果 Session 抛出异常 (包括任何SQLException), 你应该立即回滚数据库事务,调用 Session.close() ,丢弃该 Session实例。Session的某些方法可能会导致session 处于不一致的状态。所有由Hibernate抛出的异常都视为不可以恢复的。确保在 finally 代码块中调用close()方法,以关闭掉 Session

HibernateException是一个非检查期异常(这不同于Hibernate老的版本), 它封装了Hibernate持久层可能出现的大多数错误。我们的观点是,不应该强迫应用程序开发人员 在底层捕获无法恢复的异常。在大多数软件系统中,非检查期异常和致命异常都是在相应方法调用 的堆栈的顶层被处理的(也就是说,在软件上面的逻辑层),并且提供一个错误信息给应用软件的用户 (或者采取其他某些相应的操作)。请注意,Hibernate也有可能抛出其他并不属于 HibernateException的非检查期异常。这些异常同样也是无法恢复的,应该 采取某些相应的操作去处理。

Hibernate wraps SQLExceptions thrown while interacting with the database in a JDBCException. In fact, Hibernate will attempt to convert the exception into a more meaningful subclass of JDBCException. The underlying SQLException is always available via JDBCException.getCause(). Hibernate converts the SQLException into an appropriate JDBCException subclass using the SQLExceptionConverter attached to the SessionFactory. By default, the SQLExceptionConverter is defined by the configured dialect; however, it is also possible to plug in a custom implementation (see the javadocs for the SQLExceptionConverterFactory class for details). The standard JDBCException subtypes are:

  • JDBCConnectionException - 指明底层的JDBC通讯出现错误

  • SQLGrammarException - 指明发送的SQL语句的语法或者格式错误

  • ConstraintViolationException - 指明某种类型的约束违例错误

  • LockAcquisitionException - 指明了在执行请求操作时,获取 所需的锁级别时出现的错误。

  • GenericJDBCException - 不属于任何其他种类的原生异常

11.2.4. 事务超时

One extremely important feature provided by a managed environment like EJB that is never provided for non-managed code is transaction timeout. Transaction timeouts ensure that no misbehaving transaction can indefinitely tie up resources while returning no response to the user. Outside a managed (JTA) environment, Hibernate cannot fully provide this functionality. However, Hibernate can at least control data access operations, ensuring that database level deadlocks and queries with huge result sets are limited by a defined timeout. In a managed environment, Hibernate can delegate transaction timeout to JTA. This functionality is abstracted by the Hibernate Transaction object.

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中调用,此时事务超时值应该是被声明式定义的。