`

hibernate,spring 控制并发访问

阅读更多

数据库(和其他的事务系统)试图确保事务隔离性(transaction isolation),这意味着,从每个并发事务的观点来看,似乎没有其他的事务在运行。传统上而言,这已经通过锁(locking)实现了。事务可以在数据库中一个特定的数据项目上放置一把锁,暂时防止通过其他事务访问这个项目。一些现代的数据库(如Oracle和PostgreSQL)通过多版本并发控制(multiversion concurrency control,MVCC)实现事务隔离性,这种多版本并发控制通常被认为是更可伸缩的。我们将讨论假设了锁模型的隔离性;但是我们的大部分论述也适用于多版本并发控制。

数据库如何实现并发控制,这在Hibernate和 Java Persistence应用程序中是至关重要的。应用程序继承由数据库管理系统提供的隔离性保证。例如,Hibernate从不锁上内存中的任何东西。如果你认为数据库供应商有多年实现并发控制的经验,就会发现这种方法的好处。另一方面,Hibernate和Java Persistence中的一些特性(要么因为你使用它们,要么按照设计)可以改进远甚于数据库所提供的隔离性保证。

以几个步骤讨论并发控制。我们探讨最底层,研究数据库提供的事务隔离性保证。然后,看一下Hibernate和Java Persistence特性对于应用程序级的悲观和乐观并发控制,以及Hibernate可以提供哪些其他的隔离性保证。

10.2.1  理解数据库级并发

作为Hibernate应用程序的开发人员,你的任务是理解数据库的能力,以及如果特定的场景(或者按照数据完整性需求)需要,如何改变数据库的隔离性行为。让我们退一步来看。如果我们正在讨论隔离性,你可能假设两种情况,即隔离或者不隔离;现实世界中没有灰色区域。说到数据库事务,完全隔离性的代价就很高了。有几个隔离性级别(isolation level),它们一般削弱完全的隔离性,但提升了系统的性能和可伸缩性。

1.事务隔离性问题

首先,来看一下削弱完全事务隔离性时可能出现的几种现象。ANSI SQL标准根据数据库管理系统允许的现象定义了标准的事务隔离性级别:

如果两个事务都更新一个行,然后第二个事务异常终止,就会发生丢失更新(lost update),导致两处变化都丢失。这发生在没有实现锁的系统中。此时没有隔离并发事务。如图10-2所示。

 

选择

 

选择

 

更新

 

提交

 

选择

 

更新

 

回滚

 

提交

 

更新

 

更新

 

回滚

 

提交

 

图10-2 丢失更新:两个事务更新没有加锁的同一数据

如果一个事务读取由另一个还没有被提交的事务进行的改变,就发生脏读取(dirty read)。这很危险,因为由其他事务进行的改变随后可能回滚,并且第一个事务可能编写无效的数据,如图10-3所示。

图10-3 脏读取:事务A读取没有被提交的数据

如果一个事务读取一个行两次,并且每次读取不同的状态,就会发生不可重复读取(unrepeatable read)。例如,另一个事务可能已经写到这个行,并已在两次读取之间提交,如图10-4所示。

图10-4 不可重复读取:事务A执行不可重复读取两次

不可重复读取的一个特殊案例是二次丢失更新问题(second lost updates problem)。想象两个并发事务都读取一个行:一个写到行并提交,然后第二个也写到行并提交。由第一个事务所做的改变丢失了。如果考虑需要几个数据库事务来完成的应用程序对话,这个问题就特别值得关注。我们将在稍后更深入地探讨这种情况。

幻读(phantom read)发生在一个事务执行一个查询两次,并且第二个结果集包括第一个结果集中不可见的行,或者包括已经删除的行时。(不需要是完全相同的查询。)这种情形是由另一个事务在两次查询执行之间插入或者删除行造成的,如图10-5所示。

 

选择

 

选择

 

插入

 

提交

 

图10-5 幻读:事务A在第二次选择中读取新数据

既然已经了解所有可能发生的不好的情况,我们就可以定义事务隔离性级别了,并看看它们阻止了哪些问题。

2.ANSI事务隔离性级别

标准的隔离性级别由ANSI SQL标准定义,但是它们不是SQL数据库特有的。JTA也定义了完全相同的隔离性级别,稍后你将用这些级别声明想要的事务隔离性。隔离性级别的增加带来了更高成本以及严重的性能退化和可伸缩性:

l    允许脏读取但不允许丢失更新的系统,据说要在读取未提交(read uncommitted)的隔离性中操作。如果一个未提交事务已经写到一个行,另一个事务就不可能再写到这个行。但任何事务都可以读取任何行。这个隔离性级别可以在数据库管理系统中通过专门的写锁来实现。

l    允许不可重复读取但不允许脏读取的系统,据说要实现读取提交(read committed)的事务隔离性。这可以用共享的读锁和专门的写锁来实现。读取事务不会阻塞其他事务访问行。但是未提交的写事务阻塞了所有其他的事务访问该行。

l    在可重复读取(repeatable read)隔离性模式中操作的系统既不允许不可重复读取,也不允许脏读取。幻读可能发生。读取事务阻塞写事务(但不阻塞其他的读取事务),并且写事务阻塞所有其他的事务。

l    可序列化(serializable)提供最严格的事务隔离性。这个隔离性级别模拟连续的事务执行,好像事务是连续地一个接一个地执行,而不是并发地执行。序列化不可能只用低级锁实现。一定有一些其他的机制,防止新插入的行变成对于已经执行会返回行的查询的事务可见。

锁系统在DBMS中具体如何实现很不相同;每个供应商都有不同的策略。你应该查阅DBMS文档,找出更多有关锁系统的信息,如何逐步加强锁(例如从低级别到页面,到整张表),以及每个隔离性级别对于系统性能和可伸缩性有什么影响。

知道所有这些技术术语如何定义,这很好,但是它如何帮助你给应用程序选择隔离性级别呢?

3.选择隔离性级别

开发人员(包括我们自己)经常不确定要在一个产品应用程序中使用哪种事务隔离性级别。隔离性太强会损害高并发应用程序的可伸缩性。隔离性不足则可能在应用程序中导致费解的、不可重现的bug,直到系统过载运行时才会发现。

注意,我们在接下来的阐述中所指的乐观锁(optimistic locking)(利用版本),是本章稍后要解释的一个概念。你可能想要跳过这一节,并且当要在应用程序中决定隔离性级别时再回来。毕竟,选择正确的隔离性级别很大程度上取决于特定的场景。把以下讨论当作建议来读,不要把它们当作金科玉律。

在数据库的事务语义方面,Hibernate努力尽可能地透明。不过,高速缓存和乐观锁影响着这些语义。在Hibernate应用程序中要选择什么有意义的数据库隔离性级别呢?

首先,消除读取未提交隔离性级别。在不同的事务中使用一个未提交的事务变化是很危险的。一个事务的回滚或者失败将影响其他的并发事务。第一个事务的回滚可能战胜其他的事务,或者甚至可能导致它们使数据库处于一种错误的状态中。甚至由一个终止回滚的事务所做的改变也可能在任何地方被提交,因为它们可以读取,然后由另一个成功的事务传播!

其次,大多数应用程序不需要可序列化隔离性(幻读通常不成问题),并且这个隔离性级别往往难以伸缩。现有的应用程序很少在产品中使用序列化隔离性,但在某些情况下,有效地强制一个操作序列化地执行相当依赖于悲观锁(请见接下来的几节内容)。

这样就把选择读取提交还是可重复读取留给你来决定了。我们先考虑可重复读取。如果所有的数据访问都在单个原子的数据库事务中执行,这个隔离性级别就消除了一个事务可能覆盖由另一个并发事务所做变化(第二个丢失更新问题)的可能性。事务持有的读锁防止了并发事务可能希望获得的任何写锁。这是一个重要的问题,但是启用可重复读取并不是唯一的解决办法。

假设你正使用版本化(versioned)的数据,这是Hibernate可以自动完成的东西。(必需的)持久化上下文高速缓存和版本控制的组合已经提供了可重复读取隔离性的大部分优良特性。特别是,版本控制防止了二次丢失更新问题,并且持久化上下文高速缓存也确保了由一个事务加载的持久化实例状态与由其他事务所做的变化隔离开来。因此,如果你使用版本化的数据,那么对于所有数据库事务来说,读取提交的隔离性是可以接受的。

可重复读取给查询结果集(只针对数据库事务的持续期间)提供了更多的可复制性;但是因为幻读仍然可能,这似乎没有多大价值。可以在Hibernate中给一个特定的事务和数据块显式地获得可重复读取的保证(通过悲观锁)。

设置事务隔离性级别允许你给所有的数据库事务选择一个好的默认锁策略。如何设置隔离性级别呢?

4.设置隔离性级别

与数据库的每一个JDBC连接都处于DBMS的默认隔离性级别——通常是读取提交或者可重复读取。可以在DBMS配置中改变这个默认。还可以在应用程序端给JDBC连接设置事务隔离性,通过一个Hibernate配置选项:

Hibernate在启动事务之前,给每一个从连接池中获得的JDBC连接设置这个隔离性级别。对于这个选项有意义的值如下(你也可能发现它们为java.sql.Connection中的常量):

l    1——读取未提交隔离性。

l    2——读取提交隔离性。

l    3——可重复读取隔离性。

l    4——可序列化隔离性。

注意,Hibernate永远不会改变在托管环境中从应用程序服务器提供的数据库连接中获得的连接隔离性级别!可以利用应用程序服务器的配置改变默认的隔离性级别。(如果使用独立的JTA实现也一样。)

如你所见,设置隔离性级别是影响所有连接和事务的一个全局选项。给特定的事务指定一个更加限制的锁经常很有用。Hibernate和Java Persistence依赖乐观的并发控制,并且两者都允许你通过版本检查和悲观锁,获得额外的锁保证。

10.2.2  乐观并发控制

乐观的方法始终假设一切都会很好,并且很少有冲突的数据修改。在编写数据时,乐观并发控制只在工作单元结束时才出现错误。多用户的应用程序通常默认为使用读取提交隔离性级别的乐观并发控制和数据库连接。只有适当的时候(例如,当需要可重复读取的时候)才获得额外的隔离性保证;这种方法保证了最佳的性能和可伸缩性。

1.理解乐观策略

为了理解乐观并发控制,想象两个事务从数据库中读取一个特定的对象,并且两者都对它进行修改。由于数据库连接的读取提交隔离性级别,因此没有任何一个事务会遇到任何脏读取。然而,读取仍然是不可重复的,并且更新还是可能丢失。这是当你在考虑对话的时候要面对的问题,从用户的观点来看,这些是原子的事务。请见图10-6。

 

...

 

选择

 

更新

 

提交

 

提交

 

更新

 

选择

 

对话B

 

对话A

 

图10-6 对话B覆盖对对话A所做的改变

假设两个用户同时选择同一块代码。对话A中的用户先提交了变化,并且对话终止于第二个事务的成功提交。过了一会儿(可能只是一秒钟),对话B中的用户提交了变化。第二个事务也成功提交。在对话A中所做的改变已经丢失,并且(可能更糟的是)对话B中提交的数据修改可能已经基于失效的信息。

对于如何处理对话中这些第二个事务中的丢失更新,你有3种选择:

l    最晚提交生效(last commit wins)——两个事务提交都成功,且第二次提交覆盖第一个的变化。没有显示错误消息。

l    最先提交生效(first commit wins)——对话A的事务被提交,并且在对话B中提交事务的用户得到一条错误消息。用户必须获取新数据来重启对话,并再次利用没有失效的数据完成对话的所有步骤。

l    合并冲突更新(merge conflicting updates)——第一个修改被提交,并且对话B中的事务在提交时终止,带有一条错误消息。但是失败的对话B用户可以选择性地应用变化,而不是再次在对话中完成所有工作。

如果你没有启用乐观并发控制(默认情况为未启用),应用程序就会用最晚提交生效策略运行。在实践中,丢失更新的这个问题使得许多应用程序的用户很沮丧,因为他们可能发现他们的所有工作都丢失了,而没有收到任何错误消息。

很显然,最先提交生效更有吸引力。如果对话B的应用程序的用户提交,他就获得这样一条错误消息:有人已经对你要提交的数据提交了修改。你已经使用了失效数据。请用新数据重启对话。(Somebody already committed modifications to the data you’re about to commit. You’ve been working with stale data. Please restart the conversation with fresh data。)设计和编写生成这条错误消息的应用程序,并引导用户重新开始对话,这就是你的责任了。Hibernate和Java Persistence用自动乐观锁协助你,以便每当事务试图提交在数据库中带有冲突的被更新状态的对象时,就会得到一个异常。

合并冲突的变化,是最先提交生效的一种变形。不显示始终强制用户返回的错误消息,而是提供一个对话框,允许用户手工合并冲突的变化。这是最好的策略,因为没有工作丢失,应用程序的用户也不会因为乐观并发失败而受挫。然而,对于开发人员来说,提供一个对话框来合并变化比显示一条错误消息并强制用户重复所有的工作来得更加费时。是否使用这一策略,由你自己决定。

乐观并发控制可以用多种方法实现。Hibernate使用自动的版本控制。

2.在Hibernate中启用版本控制

Hibernate提供自动的版本控制。每个实体实例都有一个版本,它可以是一个数字或者一个时间戳。当对象被修改时,Hibernate就增加它的版本号,自动比较版本,如果侦测到冲突就抛出异常。因此,你给所有持久化的实体类都添加这个版本属性,来启用乐观锁:

也可以添加获取方法;但是不许应用程序修改版本号。XML格式的<version>属性映射必须立即放在标识符属性映射之后:

版本号只是一个计数值——它没有任何有用的语义值。实体表上额外的列为Hibernate应用程序所用。记住,所有访问相同数据库的其他应用程序也可以(并且或许应该)实现乐观版本控制,并利用相同的版本列。有时候时间戳是首选(或者已经存在):

理论上来说,时间戳更不安全一点,因为两个并发的事务可能都在同一毫秒点上加载和更新同一件货品;但在实践中不会发生这种情况,因为JVM通常没有精确到毫秒(你应该查阅JVM和操作系统文档所确保的精确度)。

此外,从JVM处获取的当前时间在集群环境(clustered environment)下并不一定安全,该环境中的节点可能不与时间同步。可以转换为在<timestamp>映射中利用 source="db"属性从数据库机器中获取当前的时间。并非所有的Hibernate SQL方言都支持这个属性(检查所配置的方言的源代码),每一次增加版本都始终会有命中数据库的过载。

我们建议新项目依赖包含版本号的版本,而不是时间戳。

一旦你把<version>或者<timestamp>属性添加到持久化类映射,就启用了包含版本的乐观锁。没有其他的转换。

Hibernate如何利用版本发现冲突?

3.版本控制的自动管理

涉及目前被版本控制的Item对象的每一个DML操作都包括版本检查。例如,假设在一个工作单元中,你从版本为1的数据库中加载一个Item。然后修改它的其中一个值类型属性,例如Item的价格。当持久化上下文被清除时,Hibernate侦测到修改,并把Item的版本增加到2。然后执行SQL UPDATE使这一修改在数据库中永久化:

如果另一个并发的工作单元更新和提交了同一个行,OBJ_VERSION列就不再包含值1,行也不会被更新。Hibernate检查由JDBC驱动器返回这个语句所更新的行数——在这个例子中,被更新的行数为0——并抛出StaleObjectStateException。加载Item时呈现的状态,清除时不再在数据库中呈现;因而,你正在使用失效的数据,必须通知应用程序的用户。可以捕捉这个异常,并显示一条错误消息,或者显示帮助用户给应用程序重启对话的一个对话框。

什么样的修改触发实体版本的增加?每当实体实例脏时,Hibernate就增加版本号(或者时间戳)。这包括实体的所有脏的值类型属性,无论它们是单值、组件还是集合。考虑User和 BillingDetails之间的关系,这是个一对多的实体关联:如果CreditCard修改了,相关的User版本并没有增加。如果你从账单细节的集合中添加或者删除CreditCard(或者BankAccount),User的版本就增加了。

如果你想要禁用对特定值类型属性或者集合的自动增加,就用optimistic-lock="false"属性映射它。inverse属性在这里没有什么区别。甚至如果元素从反向集合中被添加或者移除,反向集合的所有者的版本也会被更新。

如你所见,Hibernate使得对于乐观并发控制管理版本变得难以置信地轻松。如果你正在使用遗留数据库Schema或者现有的Java类,也许不可能引入版本或者时间戳和列。Hibernate提供了另一种可选的策略。

4.没有版本号或者时间戳的版本控制

如果你没有版本或者时间戳列,Hibernate仍然能够执行自动的版本控制,但是只对在同一个持久化上下文中获取和修改的对象(即相同的Session)。如果你需要乐观锁用于通过脱管对象实现的对话,则必须使用通过脱管对象传输的版本号或者时间戳。

这种可以选择的版本控制实现方法,在获取对象(或者最后一次清除持久化上下文)时,把当前的数据库状态与没有被修改的持久化属性值进行核对。可以在类映射中通过设置optimistic-lock属性来启用这项功能:

下列SQL现在被执行,用来清除Item实例的修改:

Hibernate在SQL语句的WHERE子句中,列出了所有列和它们最后知道的非失效值。如果任何并发的事务已经修改了这些值中的任何一个,或者甚至删除了行,这个语句就会再次返回被更新的行数为 0。然后Hibernate抛出一个StaleObjectStateException。

另一种方法是,如果设置optimistic- lock="dirty",Hibernate只包括限制中被修改的属性(在这个例子中,只有ITEM_PRICE)。这意味着两个工作单元可以同时修改同一个对象,并且只有当两者修改同一个值类型属性(或者外键值)时才会侦测到冲突。在大多数情况下,这对于业务实体来说并不是一种好策略。想象有两个人同时修改一件拍卖货品:一个改变价格,另一个改变描述。即使这些修改在最低级别(数据库行)没有冲突,从业务逻辑观点看它们也可能发生冲突。如果货品的描述完全改变了,还可以改变它的价格吗?如果你想要使用这个策略,还必须在实体的类映射上启用dynamic- update="true",Hibernate无法在启动时给这些动态的UPDATE语句生成SQL。

不建议在新应用程序中定义没有版本或者时间戳列的版本控制;它更慢、更复杂,如果你正在使用脱管对象,则它不会生效。

Java Persistence应用程序中的乐观并发控制与Hibernate中的几乎如出一辙。

5.用Java Persistence版本控制

Java Persistence规范假设并发数据访问通过版本控制被乐观处理。为了给一个特定的实体启用自动版本控制,需要添加一个版本属性或者字段:

同样地,可以公开一个获取方法,但不能允许应用程序修改版本值。在Hibernate中,实体的版本属性可以是任何数字类型,包括基本类型,或者Date或者Calendar类型。JPA规范只把int、 Integer、short、Short、long、Long和java.sql.Timestamp当作可移植的版本类型。

由于JPA标准没有涵盖无版本属性的乐观版本控制,因此需要Hibernate扩展,通过对比新旧状态来启用版本控制:

如果只是希望在版本检查期间比较被修改的属性,也可以转换到OptimisticLockType. DIRTY。然后你还需要设置dynamicUpdate属性为true。

Java Persistence没有对哪个实体实例修改应该触发版本增加标准化。如果你用Hibernate作为JPA提供程序,默认是一样的——每一个值类型的属性修改(包括集合元素的添加和移除)都触发版本增加。在编写本书之时,还没有在特定的属性和集合上禁用版本增加的Hibernate注解,但是已经存在一项对@OptimisticLock(excluded=true)的特性请求。你的Hibernate Annotations版本或许包括了这个选项。

Hibernate EntityManager,像任何其他Java Persistence提供程序一样,当侦测到冲突版本时,就抛出 javax.persistence.OptimisticLockException。这相当于Hibernate中原生的Stale- ObjectStateException,因此应该进行相应处理。

我们现在已经涵盖了数据库连接的基础隔离性级别,结论是你通常应该依赖来自数据库的读取提交保证。Hibernate和Java Persistence中的自动版本控制,在两个并发事务试图在同一块代码中提交修改时,防止了丢失更新。为了处理非可重复读取,你需要额外的隔离性保证。

10.2.3  获得额外的隔离性保证

有几种方法防止不可重复读取,并升级到一个更高的隔离性级别。

1.显式的悲观锁

已经讨论了把所有的数据库连接转换到一个比读取提交更高的隔离性级别,但我们的结论是,当关注应用程序的可伸缩性时,这则是一项糟糕的默认。你需要更好、仅用于一个特定的工作单元的隔离性保证。还要记住,持久化上下文高速缓存为处于持久化状态的实体实例提供可重复读取。然而,这并非永远都是足够的。

例如,对标量查询(scalar query)可能需要可重复读取:

这个工作单元执行两次读取。第一次通过标识符获取实体实例。第二次读取标量查询,再次加载已经加载的Item实体的描述。在这个工作单元中有一个小窗口,在那里,并发运行的事务可以在两次读取之间提供一个更新过的货品描述。然后第二次读取返回这个提交数据,且变量description有一个与属性i.getDescription()不同的值。

这个例子进行过简化,但仍然足以说明:如果数据库事务隔离性级别是读取提交,那么混有实体和标量读取的工作单元有多么容易受到非可重复读取的影响。

不是把所有的数据库事务转换为一个更高的、不可伸缩的隔离性级别,而是在必要时,在Hibernate Session中使用lock()方法获得更强的隔离性保证:

使用LockMode.UPGRADE,给表示Item实例的(多)行,促成了在数据库中保存的悲观锁。现在没有并发事务可以在相同数据中获得锁——也就是说,没有并发事务可以在你的两次读取之间修改数据。这段代码可以被缩短成如下:

LockMode.UPGRADE导致一个 SQL SELECT ... FOR UPDATE或者类似的东西,具体取决于数据库方言。一种变形LockMode.UPGRADE_NOWAIT,添加了一个允许查询立即失败的子句。如果没有这个子句,当无法获得锁时(可能由于一个并发事务已经有锁),数据库通常会等待。等待的持续时间取决于数据库,就像实际的SQL子句一样。

常见问题 可以使用长悲观锁吗?在Hibernate中,悲观锁的持续时间是单个数据库事务。这意味着你无法使用专门的锁,来阻塞比单个数据库事务更长的并发访问。我们认为这是好的一面,因为对于比如整个会话的持续时间来说,唯一的解决方案将是在内存(或者数据库中所谓的锁定表,lock table)中保存一个非常昂贵的锁。这种锁有时也称作离线(offline)锁。这通常是个性能瓶颈;每个数据访问都要对一个同步锁管理器进行额外的锁检查。然而,乐观锁是最完美的并发控制策略,并且在长运行对话中执行得很好。根据你的冲突解析(conflict-resolution)选项(即如果你有足够的时间实现合并变化),你应用程序的用户对此将会像对被阻塞的并发访问一样满意。他们也可能感激当其他人在看相同数据时,自己没有被锁在特定的屏幕之外。

Java Persistence出于同样的目的定义了LockModeType.READ,且EntityManager也有一个lock()方法。规范没有要求未被版本控制的实体支持这种锁模式;但Hibernate在所有的实体中都支持它,因为它在数据库中默认为悲观锁。

2.Hibernate锁模式

Hibernate支持下列其他LockMode:

l    LockMode.NONE——别到数据库中去,除非对象不处于任何高速缓存中。

l    LockMode.READ——绕过所有高速缓存,并执行版本检查,来验证内存中的对象是否与当前数据库中存在的版本相同。

l    LockMode.UPGRADE——绕过所有高速缓存,做一个版本检查(如果适用),如果支持的话,就获得数据库级的悲观升级锁。相当于Java Persistence中的LockModeType.READ。如果数据库方言不支持SELECT ... FOR UPDATE选项,这个模式就透明地退回到LockMode.READ。

l    LockMode.UPGRADE_NOWAIT——与UPGRADE相同,但如果支持的话,就使用SELECT ... FOR UPDATE NOWAIT。它禁用了等待并发锁释放,因而如果无法获得锁,就立即抛出锁异常。如果数据库SQL方言不支持NOWAIT选项,这个模式就透明地退回到 LockMode.UPGRADE。

l    LockMode.FORCE——在数据库中强制增加对象的版本,来表明它已经被当前事务修改。相当于Java Persistence中的LockModeType.WRITE。

l    LockMode.WRITE——当Hibernate已经在当前事务中写到一个行时,就自动获得它。(这是一种内部模式;你不能在应用程序中指定它。)

默认情况下,load()和get()使用LockMode.NONE。LockMode.READ对session.lock()和脱管对象最有用。这里有个例子:

这段代码在通过级联(假设从Item到Bid的关联启用了级联)保存新Bid之前,在脱管的Item实例上执行版本检查,验证该数据库行在获取之后没有被另一个事务更新。

(注意,EntityManager.lock()不重附指定的实体实例——它只对已经处于托管持久化状态的实例有效。)

Hibernate LockMode.FORCE和Java Persistence中的LockModeType.WRITE有着不同的用途。如果默认不增加版本,就利用它们强制版本更新。

3.强制增加版本

如果通过版本控制启用乐观锁,Hibernate会自动增加被修改实体实例的版本。然而,有时你想手工增加实体实例的版本,因为Hibernate不会把你的改变当成一个应该触发版本增加的修改。

想象你修改了CreditCard所有者的名称:

当这个Session被清除时,被修改的BillingDetails实例(我们假设是一张信用卡)的版本通过Hibernate自动增加了。这可能并不是你想要的东西——你可能也想增加所有者(User实例)的版本。

用LockMode.FORCE调用lock(),增加一个实体实例的版本:

现在,任何使用相同User行的并发工作单元都知道这个数据被修改了,即使只有被你认为是整个聚合的一部分的其中一个值被修改了。这种技术在许多情况下都有用,例如当你修改一个对象,并且想要增加聚合的根对象版本时。另一个例子是对一件拍卖货品出价金额的修改(如果这些金额是不可变的):利用一个显式的版本增加,可以指出这件货品已经被修改,即使它的值类型属性或者集合都没有发生改变。利用Java Persistence的同等调用是em.lock(o,LockModeType.WRITE)。

现在,你具备了编写更复杂工作单元和创建对话的所有知识。但是,我们需要提及事务的最后一个方面,因为它在使用JPA的更复杂对话中变得必不可少。你必须理解自动提交如何工作,以及在实践中非事务数据访问意味着什么。

分享到:
评论
1 楼 tophei 2011-09-27  
[color=green][/color]

相关推荐

Global site tag (gtag.js) - Google Analytics