Hibernate.org Community Documentation

- 上一页
- 下一页

## 第 13 章 事务和并发

Hibernate 的事务和并发控制很容易掌握。Hibernate 直接使用 JDBC 连接和 JTA 资源，不添加任何附加锁定行为。我们强烈推荐你花点时间了解 JDBC 编程，ANSI SQL 查询语言和你使用的数据库系统的事务隔离规范。

Hibernate 不锁定内存中的对象。你的应用程序会按照你的数据库事务的隔离级别规定的那样运作。幸亏有了 `Session`，使得 Hibernate 通过标识符查找，和实体查询（不是返回标量值的报表查询）提供了可重复的读取（Repeatable reads）功能，`Session` 同时也是事务范围内的缓存（cache）。

除了对自动乐观并发控制提供版本管理，针对行级悲观锁定，Hibernate 也提供了辅助的（较小的）API，它使用了 `SELECT FOR UPDATE` 的 SQL 语法。本章后面会讨论乐观并发控制和这个API。

我们从 `Configuration`层、`SessionFactory` 层，和 `Session` 层开始讨论 Hibernate 的并行控制、数据库事务和应用程序的长事务。

## 13.1. Session 和事务范围（transaction scope）

`SessionFactory` 对象的创建代价很昂贵，它是线程安全的对象，它为所有的应用程序线程所共享。它只创建一次，通常是在应用程序启动的时候，由一个 `Configuraion` 的实例来创建。

`Session` 对象的创建代价比较小，是非线程安全的，对于单个请求，单个会话、单个的 工作单元而言，它只被使用一次，然后就丢弃。只有在需要的时候，一个 `Session` 对象 才会获取一个 JDBC 的 `Connection`（或一个`Datasource`）对象，因此假若不使用的时候它不消费任何资源。

此外我们还要考虑数据库事务。数据库事务应该尽可能的短，降低数据库中的锁争用。数据库长事务会阻止你的应用程序扩展到高的并发负载。因此，假若在用户思考期间让数据库事务开着，直到整个工作单元完成才关闭这个事务，这绝不是一个好的设计。

一个操作单元（Unit of work）的范围是多大？单个的 Hibernate `Session` 能跨越多个数据库事务吗？还是一个 `Session` 的作用范围对应一个数据库事务的范围？应该何时打开 `Session`，何时关闭 `Session`，你又如何划分数据库事务的边界呢？我们将在后续章节解决这些问题。

### 13.1.1. 操作单元（Unit of work）

First, let's define a unit of work. A unit of work is a design pattern described by Martin Fowler as “ [maintaining] a list of objects affected by a business transaction and coordinates the writing out of changes and the resolution of concurrency problems. ”[PoEAA] In other words, its a series of operations we wish to carry out against the database together. Basically, it is a transaction, though fulfilling a unit of work will often span multiple physical database transactions (see 第 13.1.2 节 “长对话”). So really we are talking about a more abstract notion of a transaction. The term "business transaction" is also sometimes used in lieu of unit of work.

首先，别用 session-per-operation 这种反模式了，也就是说，在单个线程中， 不要因为一次简单的数据库调用，就打开和关闭一次 `Session`！数据库事务也是如此。 应用程序中的数据库调用是按照计划好的次序，分组为原子的操作单元。（注意，这也意味着，应用程 序中，在单个的 SQL 语句发送之后，自动事务提交（auto-commit）模式失效了。这种模式专门为SQL控制台操作设计的。 Hibernate 禁止立即自动事务提交模式，或者期望应用服务器禁止立即自动事务提交模式。）数据库事务绝不是可有可无的，任何与数据库之间的通讯都必须在某个事务中进行，不管你是在读还是在写数据。对读数据而言，应该避免 auto-commit 行为，因为很多小的事务比一个清晰定义的工作单元性能差。后者也更容易维护和扩展。

在多用户的 client/server 应用程序中，最常用的模式是 每个请求一个会话（session-per-request）。 在这种模式下，来自客户端的请求被发送到服务器端（即 Hibernate 持久化层运行的地方），一个新的 Hibernate `Session` 被打开，并且执行这个操作单元中所有的数据库操作。一旦操作完成（同时对客户端的响应也准备就绪），session 被同步，然后关闭。你也可以使用单 个数据库事务来处理客户端请求，在你打开 `Session` 之后启动事务，在你关闭 `Session` 之前提交事务。会话和请求之间的关系是一对一的关系，这种模式对 于大多数应用程序来说是很棒的。

实现才是真正的挑战。Hibernate 内置了对"当前 session（current session）" 的管理，用于简化此模式。你要做的一切就是在服务器端要处理请求的时候，开启事务，在响应发送给客户之前结束事务。你可以用任何方式来完成这一操作，通常的方案有 `ServletFilter`，在 service 方法中进行 pointcut 的 AOP 拦截器，或者 proxy/interception 容器。EJB 容器是实现横切诸如 EJB session bean 上的事务分界，用 CMT 对事务进行声明等方面的标准手段。假若你决定使用编程式的事务分界，请参考本章后面讲到的 Hibernate `Transaction` API，这对易用性和代码可移植性都有好处。

Your application code can access a "current session" to process the request by calling `sessionFactory.getCurrentSession()`. You will always get a `Session` scoped to the current database transaction. This has to be configured for either resource-local or JTA environments, see 第 2.3 节 “上下文相关的会话（Contextual Session）”.

有时，将 `Session` 和数据库事务的边界延伸到"展示层被渲染后"会带来便利。有些 serlvet 应用程序在对请求进行处理后，有个单独的渲染期，这种延伸对这种程序特别有用。假若你实现你自己的拦截器，把事务边界延伸到展示层渲染结束后非常容易。然而，假若你依赖有容器管理事务的 EJB，这就不太容易了，因为事务会在 EJB 方法返回后结束，而那是在任何展示层渲染开始之前。请访问 Hibernate 网站和论坛，你可以找到 Open Session in View 这一模式的提示和示例。

### 13.1.2. 长对话

session-per-request 模式不仅仅是一个可以用来设计操作单元的有用概念。很多业务处理都需 要一系列完整的与用户之间的交互，而这些用户是指对数据库有交叉访问的用户。在基于 web 的应用和企业应用中，跨用户交互的数据库事务是无法接受的。考虑下面的例子：

- 在界面的第一屏，打开对话框，用户所看到的数据是被一个特定的 `Session` 和数据 库事务载入（load）的。用户可以随意修改对话框中的数据对象。

- 5 分钟后，用户点击“保存”，期望所做出的修改被持久化；同时他也期望自己是唯一修改这个信息的人，不会出现修改冲突。

从用户的角度来看，我们把这个操作单元称为长时间运行的对话（conversation），或者应用事务（application transaction）。在你的应用程序中，可以有很多种方法来实现它。

头一个幼稚的做法是，在用户思考的过程中，保持 `Session` 和数据库事务是打开的，保持数据库锁定，以阻止并发修改，从而保证数据库事务隔离级别和原子操作。这种方式当然是一个反模式，因为锁争用会导致应用程序无法扩展并发用户的数目。

很明显，我们必须使用多个数据库事务来实现这个对话。在这个例子中，维护业务处理的 事务隔离变成了应用程序层的部分责任。一个对话通常跨越多个数据库事务。如果仅仅只有一个数据库事务（最后的那个事务）保存更新过的数据，而所有其他事务只是单纯的读取数据（例如在一个跨越多个请求/响应周期的向导风格的对话框中），那么应用程序事务将保证其原子性。这种方式比听起来还要容易实现，特别是当你使用了 Hibernate 的下述特性的时候：

- 自动版本化：Hibernate 能够自动进行乐观并发控制，如果在用户思考的过程中发生并发修改，Hibernate 能够自动检测到。一般我们只在对话结束时才检查。

- 脱管对象（Detached Objects）：如果你决定采用前面已经讨论过的 session-per-request 模式，所有载入的实例在用户思考的过程中都处于与 Session 脱离的状态。Hibernate 允许你把与 Session 脱离的对象重新关联到 Session 上，并且对修改进行持久化，这种模式被称为 session-per-request-with-detached-objects。自动版本化被用来隔离并发修改。

- Extended (or Long) Session：Hibernate 的 `Session` 可以在数据库事务提交之后和底层的 JDBC 连接断开，当一个新的客户端请求到来的时候，它又重新连接上底层的 JDBC 连接。这种模式被称之为session-per-conversation，这种情况可 能会造成不必要的 Session 和 JDBC 连接的重新关联。自动版本化被用来隔离并发修改，`Session` 通常不允许自动 flush，而是显性地 flush。

session-per-request-with-detached-objects 和 session-per-conversation 各有优缺点，我们在本章后面乐观并发控制那部分再进行讨论。

### 13.1.3. 关注对象标识（Considering object identity）

应用程序可能在两个不同的 `Session` 中并发访问同一持久化状态，但是，一个持久化类的实例无法在两个 `Session` 中共享。因此有两种不同的标识语义：

数据库标识

  `foo.getId().equals( bar.getId() )`

JVM 标识

  `foo==bar`

对于那些关联到 特定 `Session`（也就是在单个 `Session` 的范围内）上的对象来说，这两种标识的语义是等价的，与数据库标识对应的 JVM 标识是由 Hibernate 来保证的。不过，当应用程序在两个不同的 session 中并发访问具有同一持久化标识的业务对象实例的时候，这个业务对象的两个实例事实上是不相同的（从 JVM 识别来看）。这种冲突可以通过在同步和提交的时候使用自动版本化和乐观锁定方法来解决。

这种方式把关于并发的头疼问题留给了 Hibernate 和数据库；由于在单个线程内，操作单元中的对象识别不 需要代价昂贵的锁定或其他意义上的同步，因此它同时可以提供最好的可伸缩性。只要在单个线程只持有一个 `Session`，应用程序就不需要同步任何业务对象。在 `Session` 的范围内，应用程序可以放心的使用 `==` 进行对象比较。

不过，应用程序在 `Session` 的外面使用 `==` 进行对象比较可能会 导致无法预期的结果。在一些无法预料的场合，例如，如果你把两个脱管对象实例放进同一个 `Set` 的时候，就可能发生。这两个对象实例可能有同一个数据库标识（也就是说， 他们代表了表的同一行数据），从 JVM 标识的定义上来说，对脱管的对象而言，Hibernate 无法保证他们 的的 JVM 标识一致。开发人员必须覆盖持久化类的 `equals()` 方法和 `hashCode()` 方法，从而实现自定义的对象相等语义。警告：不要使用数据库标识来实现对象相等，应该使用业务键值，由唯一的，通常不变的属性组成。当一个瞬时对象被持久化的时候，它的数据库标识会发生改变。如果一个瞬时对象（通常也包括脱管对象实例）被放入一个 `Set`，改变它的 hashcode 会导致与这个 `Set` 的关系中断。虽 然业务键值的属性不象数据库主键那样稳定不变，但是你只需要保证在同一个 `Set` 中的对象属性的稳定性就足够了。请到 Hibernate 网站去寻求这个问题更多的详细的讨论。请注意，这不是一个有关 Hibernate 的问题，而仅仅是一个关于 Java 对象标识和判等行为如何实现的问题。

### 13.1.4. 常见问题

决不要使用反模式 session-per-user-session 或者 session-per-application（当然，这个规定几乎没有例外）。请注意，下述一些问题可能也会出现在我们推荐的模式中，在你作出某个设计决定之前，请务必理解该模式的应用前提。

- `Session` 对象是非线程安全的。如果一个 `Session` 实例允许共享的话，那些支持并发运行的东东，例如 HTTP request，session beans 或者是 Swing workers，将会导致出现资源争用（race condition）。如果在 `HttpSession` 中有 Hibernate 的 `Session` 的话（稍后讨论），你应该考虑同步访问你的 Http session。 否则，只要用户足够快的点击浏览器的“刷新”，就会导致两个并发运行线程使用同一个 `Session`。

- 一个由 Hibernate 抛出的异常意味着你必须立即回滚数据库事务，并立即关闭 `Session`（稍后会展开讨论）。如果你的 `Session` 绑定到一个应用程序上，你必须停止该应用程序。回滚数据库事务并不会把你的业务对象退回到事务启动时候的状态。这意味着数据库状态和业务对象状态不同步。通常情况下，这不是什么问题，因为异常是不可恢复的，你必须在回滚之后重新开始执行。

- The `Session` caches every object that is in a persistent state (watched and checked for dirty state by Hibernate). If you keep it open for a long time or simply load too much data, it will grow endlessly until you get an OutOfMemoryException. One solution is to call `clear()` and `evict()` to manage the `Session` cache, but you should consider a Stored Procedure if you need mass data operations. Some solutions are shown in 第 15 章 批量处理（Batch processing）. Keeping a `Session` open for the duration of a user session also means a higher probability of stale data.

## 13.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，刷出）前面已经讨论过了，我们现在进一步考察在托管和非托管环境下的事务声明和异常处理。

### 13.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 的 第 11.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"`。

### 13.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()`。）

### 13.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`：不属于任何其他种类的原生异常。

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

## 13.3. 乐观并发控制（Optimistic concurrency control）

唯一能够同时保持高并发和高可伸缩性的方法就是使用带版本化的乐观并发控制。版本检查使用版本号、 或者时间戳来检测更新冲突（并且防止更新丢失）。Hibernate 为使用乐观并发控制的代码提供了三种可 能的方法，应用程序在编写这些代码时，可以采用它们。我们已经在前面应用程序对话那部分展示了 乐观并发控制的应用场景，此外，在单个数据库事务范围内，版本检查也提供了防止更新丢失的好处。

### 13.3.1. 应用程序级别的版本检查（Application version checking）

未能充分利用 Hibernate 功能的实现代码中，每次和数据库交互都需要一个新的 `Session`，而且开发人员必须在显示数据之前从数据库中重新载入所有的持久化对象实例。这种方式迫使应用程序自己实现版本检查来确保对话事务的隔离，从数据访问的角度来说是最低效的。这种使用方式和 entity EJB 最相似。

    // foo is an instance loaded by a previous Session

    session = factory.openSession();

    Transaction t = session.beginTransaction();

    int oldVersion = foo.getVersion();

    session.load( foo, foo.getKey() ); // load the current state

    if ( oldVersion != foo.getVersion() ) throw new StaleObjectStateException();

    foo.setProperty("bar");

    t.commit();

    session.close();

`version` 属性使用 `<version>` 来映射，如果对象是脏数据，在同步的时候，Hibernate 会自动增加版本号。

当然，如果你的应用是在一个低数据并发环境下，并不需要版本检查的话，你照样可以使用这种方式，只不过跳过版本检查就是了。在这种情况下，最晚提交生效 （last commit wins）就是你的长对话的默认处理策略。请记住这种策略可能会让应用软件的用户感到困惑，因为他们有可能会碰上更新丢失掉却没有出错信息，或者需要合并更改冲突的情况。

很明显，手工进行版本检查只适合于某些软件规模非常小的应用场景，对于大多数软件应用场景来说并不现实。通常情况下，不仅是单个对象实例需要进行版本检查，整个被修改过的关联对象图也都需要进行版本检查。作为标准设计范例，Hibernate 使用扩展周期的 `Session` 的方式，或者脱管对象实例的方式来提供自动版本检查。

### 13.3.2. 扩展周期的 session 和自动版本化

单个 `Session` 实例和它所关联的所有持久化对象实例都被用于整个对话，这被称为 session-per-conversation。Hibernate 在同步的时候进行对象实例的版本检查，如果检测到并发修改则抛出异常。由开发人员来决定是否需要捕获和处理这个异常（通常的抉择是给用户 提供一个合并更改，或者在无脏数据情况下重新进行业务对话的机会）。

在等待用户交互的时候， `Session` 断开底层的 JDBC 连接。这种方式以数据库访问的角度来说是最高效的方式。应用程序不需要关心版本检查或脱管对象实例的重新关联，在每个数据库事务中，应用程序也不需要载入读取对象实例。

    // foo is an instance loaded earlier by the old session

    Transaction t = session.beginTransaction(); // Obtain a new JDBC connection, start transaction

    foo.setProperty("bar");

    session.flush();    // Only for last transaction in conversation

    t.commit();         // Also return JDBC connection

    session.close();    // Only for last transaction in conversation

`foo` 对象知道它是在哪个 `Session` 中被装入的。在一个旧 session 中开启一个新的数据库事务，会导致 session 获取一个新的连接，并恢复 session 的功能。将数据库事务提交，使得 session 从 JDBC 连接断开，并将此连接交还给连接池。在重新连接之后，要强制对你没有更新的数据进行一次版本检查，你可以对所有可能被其他事务修改过的对象，使用参数 `LockMode.READ` 来调用 `Session.lock()`。你不用 lock 任何你正在更新的数据。一般你会在扩展的 `Session` 上设置 `FlushMode.NEVER`，因此只有最后一个数据库事务循环才会真正的把整个对话中发生的修改发送到数据库。因此，只有这最后一次数据库事务才会包含 `flush()` 操作，然后在整个对话结束后，还要 `close()` 这个 session。

如果在用户思考的过程中，`Session` 因为太大了而不能保存，那么这种模式是有问题的。举例来说，一个 `HttpSession` 应该尽可能的小。由于 `Session` 是一级缓存，并且保持了所有被载入过的对象，因此我们只应该在那些少量的 request/response 情况下使用这种策略。你应该只把一个 `Session` 用于单个对话，因为它很快就会出现脏数据。

## 注意

注意，早期的 Hibernate 版本需要明确的对 `Session` 进行 disconnect 和 reconnect。这些方法现在已经过时了，打开事务和关闭事务会起到同样的效果。

此外，也请注意，你应该让与数据库连接断开的 `Session` 对持久层保持关闭状态。换句话说，在三层环境中，使用有状态的 EJB session bean 来持 有`Session`， 而不要把它传递到 web 层（甚至把它序列化到一个单独的层），保存在 `HttpSession` 中。

扩展 session 模式，或者被称为每次对话一个session（session-per-conversation），自动管理当前 session 上下文联用的时候会更困难。你需要提供你自己的 `CurrentSessionContext` 实现。请参阅 Hibernate Wiki 以获得示例。

### 13.3.3. 脱管对象（deatched object）和自动版本化

这种方式下，与持久化存储的每次交互都发生在一个新的 `Session` 中。然而，同一持久化对象实例可以在多次与数据库的交互中重用。应用程序操纵脱管对象实例 的状态，这个脱管对象实例最初是在另一个 `Session` 中载入的，然后调用 `Session.update()`，`Session.saveOrUpdate()`，或者 `Session.merge()` 来重新关联该对象实例。

    // foo is an instance loaded by a previous Session

    foo.setProperty("bar");

    session = factory.openSession();

    Transaction t = session.beginTransaction();

    session.saveOrUpdate(foo); // Use merge() if "foo" might have been loaded already

    t.commit();

    session.close();

Hibernate 会再一次在同步的时候检查对象实例的版本，如果发生更新冲突，就抛出异常。

如果你确信对象没有被修改过，你也可以调用 `lock()` 来设置 `LockMode.READ`（绕过所有的缓存，执行版本检查），从而取代 `update()` 操作。

### 13.3.4. 定制自动版本化行为

对于特定的属性和集合，通过为它们设置映射属性 `optimistic-lock` 的值为 `false`，来禁止 Hibernate 的版本自动增加。这样的话，如果该属性脏数据，Hibernate 将不再增加版本号。

遗留系统的数据库 Schema 通常是静态的，不可修改的。或者，其他应用程序也可能访问同一数据库，根本无法得知如何处理版本号，甚至时间戳。在以上的所有场景中，实现版本化不能依靠数据库表的某个特定列。在 `<class>` 的映射中设置 `optimistic-lock="all"` 可以在没有版本或者时间戳属性映射的情况下实现版本检查，此时 Hibernate 将比较一行记录的每个字段的状态。请注意，只有当 Hibernate 能够比较新旧状态的情况下，这种方式才能生效，也就是说，你必须使用单个长生命周期 `Session` 模式，而不能使用 session-per-request-with-detached-objects 模式。

有些情况下，只要更改不发生交错，并发修改也是允许的。当你在 `<class>` 的映射中设置 `optimistic-lock="dirty"`，Hibernate 在同步的时候将只比较有脏数据的字段。

在以上所有场景中，不管是专门设置一个版本/时间戳列，还是进行全部字段/脏数据字段比较，Hibernate 都会针对每个实体对象发送一条 `UPDATE`（带有相应的 `WHERE` 语句 ）的 SQL 语句来执行版本检查和数据更新。如果你对关联实体 设置级联关系使用传播性持久化（transitive persistence），那么 Hibernate 可能会执行不必 要的update语句。这通常不是个问题，但是数据库里面对 on update 点火 的触发器可能在脱管对象没有任何更改的情况下被触发。因此，你可以在 `<class>` 的映射中，通过设置`select-before-update="true"` 来定制这一行为，强制 Hibernate `SELECT` 这个对象实例，从而保证，在更新记录之前，对象的确是被修改过。

## 13.4. 悲观锁定（Pessimistic Locking）

用户其实并不需要花很多精力去担心锁定策略的问题。通常情况下，只要为 JDBC 连接指定一下隔离级别，然后让数据库去搞定一切就够了。然而，高级用户有时候希望进行一个排它的悲观锁定，或者在一个新的事务启动的时候，重新进行锁定。

Hibernate 总是使用数据库的锁定机制，从不在内存中锁定对象。

类 `LockMode` 定义了 Hibernate 所需的不同的锁定级别。一个锁定可以通过以下的机制来设置：

- 当 Hibernate 更新或者插入一行记录的时候，锁定级别自动设置为 `LockMode.WRITE`。

- 当用户显式的使用数据库支持的 SQL 格式 `SELECT ... FOR UPDATE` 发送 SQL 的时候，锁定级别设置为 `LockMode.UPGRADE`。

- 当用户显式的使用 Oracle 数据库的 SQL 语句 `SELECT ... FOR UPDATE NOWAIT` 的时候，锁定级别设置 `LockMode.UPGRADE_NOWAIT`。

- 当 Hibernate 在“可重复读”或者是“序列化”数据库隔离级别下读取数据的时候，锁定模式自动设置为 `LockMode.READ`。这种模式也可以通过用户显式指定进行设置。

- `LockMode.NONE` 代表无需锁定。在 `Transaction` 结束时， 所有的对象都切换到该模式上来。与 session 相关联的对象通过调用 `update()` 或者 `saveOrUpdate()` 脱离该模式。

"显式的用户指定"可以通过以下几种方式之一来表示：

- 调用 `Session.load()` 的时候指定`锁定模式（LockMode）`。

- 调用 `Session.lock()`。

- 调用 `Query.setLockMode()`。

如果在 `UPGRADE` 或者 `UPGRADE_NOWAIT` 锁定模式下调用 `Session.load()`，并且要读取的对象尚未被 session 载入过，那么对象通过 `SELECT ... FOR UPDATE` 这样的 SQL 语句被载入。如果为一个对象调用 `load()` 方法时，该对象已经在另一个较少限制的锁定模式下被载入了，那么 Hibernate 就对该对象调用 `lock()` 方法。

如果指定的锁定模式是 `READ`，`UPGRADE` 或 `UPGRADE_NOWAIT`，那么 `Session.lock()` 就执行版本号检查。（在 `UPGRADE` 或者 `UPGRADE_NOWAIT` 锁定模式下，执行 `SELECT ... FOR UPDATE`这样的SQL语句。）

如果数据库不支持用户设置的锁定模式，Hibernate 将使用适当的替代模式（而不是扔出异常）。这一点可以确保应用程序的可移植性。

## 13.5. 连接释放模式（Connection Release Modes）

Hibernate 关于 JDBC 连接管理的旧（2.x）行为是，`Session` 在第一次需要的时候获取一个连接，在 session 关闭之前一直会持有这个连接。Hibernate 引入了连接释放的概念，来告诉 session 如何处理它的 JDBC 连接。注意，下面的讨论只适用于采用配置 `ConnectionProvider` 来提供连接的情况，用户自己提供的连接与这里的讨论无关。通过 `org.hibernate.ConnectionReleaseMode` 的不同枚举值来使用不用的释放模式：

- `ON_CLOSE`：基本上就是上面提到的老式行为。Hibernate session 在第一次需要进行 JDBC 操作的时候获取连接，然后持有它，直到 session 关闭。

- `AFTER_TRANSACTION`：在 `org.hibernate.Transaction` 结束后释放连接。

- `AFTER_STATEMENT`（也被称做积极释放）：在每一条语句被执行后就释放连接。但假若语句留下了与 session 相关的资源，那就不会被释放。目前唯一的这种情形就是使用 `org.hibernate.ScrollableResults`。

`hibernate.connection.release_mode` 配置参数用来指定使用哪一种释放模式。可能的值有：

- `auto`（默认）：这一选择把释放模式委派给 `org.hibernate.transaction.TransactionFactory.getDefaultReleaseMode()` 方法。对 JTATransactionFactory 来说，它会返回 ConnectionReleaseMode.AFTER_STATEMENT；对　JDBCTransactionFactory　来说，则是ConnectionReleaseMode.AFTER_TRANSACTION。很少需要修改这一默认行为，因为假若设置不当，就会带来 bug，或者给用户代码带来误导。

- `on_close`：使用 ConnectionReleaseMode.ON_CLOSE。这种方式是为了向下兼容的,但是已经完全不被鼓励使用了。

- `after_transaction`：使用 ConnectionReleaseMode.AFTER_TRANSACTION。这一设置不应该在 JTA 环境下使用。也要注意，使用 ConnectionReleaseMode.AFTER_TRANSACTION 的时候，假若session 处于 auto-commit 状态，连接会像 AFTER_STATEMENT 那样被释放。

- `after_statement`：使用 ConnectionReleaseMode.AFTER_STATEMENT。除此之外，会查询配置的 `ConnectionProvider`，是否它支持这一设置（`supportsAggressiveRelease()`）。假若不支持，释放模式会被设置为 ConnectionReleaseMode.AFTER_TRANSACTION。只有在你每次调用 `ConnectionProvider.getConnection()` 获取底层 JDBC 连接的时候，都可以确信获得同一个连接的时候，这一设置才是安全的；或者在 auto-commit 环境中，你可以不管是否每次都获得同一个连接的时候，这才是安全的。

版权 © 2004 Red Hat, Inc.

- 上一页第 12 章 Read-only entities
- 上一级
- 起始页
- 下一页第 14 章 拦截器与事件（Interceptors and events）