81.有时候,通过外部联结,可以在单个查询中更好地加载实体实例和集合(实体可能有一个空的集合,因此无法使用内部联结)。如果想要应用这个主动抓取,就不要为集合声明装载程序引用。实体装载程序负责集合的获取:
<sql-query name="loadUser">
<return alias="u" class="User"/>
<return-join alias="i" property="u.items"/>
select {u.*},{i.*} from USERS u
left outer join ITEM i on u.USER_ID=i.SELLER_ID
where u.USER_ID=?
</sql-query>
注意如何使用<return-join>元素把别名绑定到实体的集合属性,有效地把两个别名链接在一起。还要进一步注意,如果想要在一个原始查询中主动抓取一对一和多对一关联的实体,这个方法也有效。
Hibernate在启动时生成所有琐碎的CRUD SQL。它在内部高速缓存SQL声明以备未来之用,这样避免了对最普通操作的SQL生成的任何运行时的成本。
对于给每个实体或者集合,可以在<sql-insert>、<sql-delete>和<sql-update>内部响应定义定制的CUD SQL语句:
82.考虑check="none"属性。为了正确的(且如果启用了)乐观锁,Hibernate需要知道这个定制更新操作是否成功。通常,对于动态生成的SQL,Hibernate查看从操作中返回的被更新行的数量。如果操作没有或者无法更新任何行,就会出现乐观锁失败。如果你编写自己定制的SQL操作,也可以定制这个行为。
利用check="none",Hibernate期待你的定制过程内部处理失败的更新(例如,通过给需要被更新的行做一个版本检测),并期待过程在有东西出错时抛出异常。
SQL错误被Hibernate抓住,并被转换为一个随后可以在应用程序代码中处理的乐观锁异常。检测属性的其他选项如下:
(1)如果启用check="count",Hibernate就会利用简单的JDBC API检查被修改的行数。这是默认的,在你编写动态的SQL而没用存储过程时使用。
(2)如果启用check=”param“,Hibern就会保存一个OUT参数,获取存储过程调用的返回值。你需要把一个额外的问号添加到调用,并在存储过程中返回这(第一)个OUT参数中DML操作的行数。然后Hibernate为你验证被修改的行数。
<database-object>
<create>
[CREATE statement]
</ceate>
<drop>
[DROP statement]
</drop>
<dialect-scope name="org.hibernate.dialect.Oracle9Dialect"/>
<dialect-scope name="org.hibernate.dialect.OracleDialect"/>
</database-object>
<dialect-scope>元素把定制的CREATE或者DROP语句限制为一个特定的被配置数据库方言组,如果你正在几个系统中部署并且需求不同的定制时,它很有用。
如果需要更多对于生成的DDL可编程式的空值,就实现AuxiliaryDatabaseObject接口。Hibernate包含一个可以子类化的便利实现;然后你可以有选择地覆盖方法。
83.任何包含持久化状态的应用程序都必须与持久化服务交互,每当它需要把保存在内存中的状态传播到数据库的时候(反之亦然)。换句话说,你必须调用Hibernate(或者Java persistence)接口来保存和加载对象。
(1)瞬时状态
利用new操作符实例化的对象并不立即就是持久化的。它们的状态时瞬时的(transient),意味着它们不与任何数据库的表行相关联。因此一旦不再被其他的对象引用时,它们的状态立即丢失。
Hibernate和Java Persistence任务所有的瞬时实例都要变成非事务的;持久化上下文不知道瞬时实例的任何修改。这意味着Hibernate不给瞬时对象提供任何回滚功能。
只被其他瞬时实例引用的对象也默认为瞬时。对于从瞬时转变为持久化状态的实例,要变成托管,需要调用一个持久化管理器,或者从已经持久化的实例中创建一个引用。
(2)持久化对象
持久化实例是一个包含数据库同一性的实体实例。这意味着持久化且被托管的实例具有设置称为其数据库标识符的主键值。(当这个标识符被分配到持久化实例的时候,有一些变化形式)。
持久化实例可能是被应用程序实例化的对象,然后通过在持久化管理器上调用其中一种方法变成持久化。它们甚至可能是当从另一个已经托管的持久化对象中创建引用时编程持久化的对象。或者,持久化实例也可能是通过执行查询、标识符查找、或者开始从另一个持久化实例导航对象图,从数据库中获取的一个实例。
持久化实例始终与持久化上下文(persistance context)关联。Hibernate告诉缓存它们,并且可以侦测到它们是否已经被应用程序修改。
(3)移除对象
可以通过几种方式删除实体实例:例如,可以用持久化管理器的一个显示操作把它移除。如果移除所有对它的引用,它可能也变成可以删除的了,这项特性只有在包含Hibernate扩展设置(实体的孤儿删除)的Hibernate或者Java Persistence中才可用。
如果一个对象已经被计划在一个工作单元结束时删除,它就是处于移除状态,但仍然由持久化上下文托管,知道工作单元完成。换句话说,移除对象不应该被重用,因为一旦工作单元完成,它就将立即从数据库中被删除。你也应该放弃在应用程序中保存着的任何对它的引用(当然,是在你用完它之后)
(4)脱管对象
要理解脱管(detached)对象,你需要考虑实例的一种典型的转变:它先是瞬时的,因为它刚刚在应用程序中创建。现在通过在持久化管理器中调用一个操作使它变成持久化。所有这些都发生在单个工作单元中,并且这个工作单元的持久化上下文在某个时间点(当产生一个SQL的INSERT)与数据库同步。
现在工作单元完成了,持久化上下文也关闭了。但是应用程序仍然有一个句柄(handle):对被保存实例的一个引用。只要持久化上下文是活动的,这个实例的状态就是持久化的。在工作单元结束时,持久化上下文关闭。
这些对象当作脱管(detached),表示它们的状态不在保证与数据库状态同步,不再被附加到持久化上下文中,并仍然包含持久化数据(可能很快会失效)。可以继续使用脱管对象并修改它。但有时候你或许想要使那些变化变成持久化——换句话说,把脱管实例变回到持久化状态。
Hibernate提供重附(reattachment)和合并(merging)两种操作来处理这种情况。Java Persistence只对合并标准化。这些特性对如何设计多层应用程序有着深刻的影响。从一个持久化上下文返回对象到表现出,并且随后在一个新的持久化上下文中重用他们的能力是Hibernate和Java Persistence的主要卖点。它让你能够创建跨越用户思考时间的长工作单元。称这种长期运行的工作单元为对话(conversation)。
84.持久化上下文不是你在应用程序中所见到的东西;它不是一个可以调用的API。在Hibernate应用程序中,假设一个Session有一个内部的持久化上下文。在Java Persistence应用程序中,EntityManager具有持久化上下文。一个工作单元中所有处于持久化状态和托管状态的实体都被高速缓存在这个上下文中。
持久化上下文之所有有用,基于以下几个原因:
(1)Hibernate可以进行自动的脏检查和事务迟写。
(2)Hibernate可以用持久化上下文作为一级高速缓存。
(3)Hibernate可以保证Java对象同一性的范围。
(4)Hibernate可以把持久化上下文扩展到整个会话。
1.自动脏检查
持久化实例托管在一个持久化上下文中——它们的状态在工作单元结束时与数据库同步。当一个工作单元结束时,保存在内存中的状态通过SQL INSERT、UPDATE和DELETE语句(DML)的执行被传播到数据库。这个过程也可能发生在其他时间点。例如,Hibernate可能在查询执行之前与数据库同步。
Hibernate没有于工作单元结束时在内存中更新每一个单独的持久化对象的数据行。
利用透明的的事务迟写(transparent transaction-level write-behind),Hibernate尽可能迟地把状态变化传播到数据库,但是从应用程序中隐藏这个细节。通过尽可能迟地执行DML(趋向于数据库事务的结束),Hibernate试图保证数据库中的锁时间尽可能短。(DML通常在数据中创建一直被保存到事务结束的锁)。
如果你想要只更新被修改的列,可以通过在类映射中设置dynamic-update="true"启用动态的SQL生成。对新记录的插入实现相同的机制,并且可以用dynamic-insert="true"启用INSERT语句的运行时生成。当一张表中有特别多的列(假设,超过50列),我们建议考虑这种设置。
Hibernate默认把一个对象的旧快照与同步时的快照进行比较,侦测任何需要更新数据库状态的修改。可以通过org.hibernate.Interceptor给Session提供一个定制的findDirty()方法来实现自己的子程序。
2.持久化上下文高速缓存
持久化上下文是持久化实体实例的一个高速缓存。这意味着它记住了你已经在特定的工作单元中处理过的所有持久化实体实例。自动脏检查是这个高速缓存的好处之一。另一个好处是对实体的可重复读取(repeatable read),以及工作范围高速缓存单元的性能优势。
持久化上下文高速缓存有时候帮助避免不必要的数据库流量;但更重要的是,它确保了:
(1)持久层在对象图中出现循环引用时,不会受到堆栈溢出的影响
(2)工作单元结束时,永远不能有相同数据行的冲突表示法。在持久化的上下文中,最多一个对象表示任何一个数据库行。对该对象进行的所有变化都可以被安全地写到数据库中。
(3)同样地,在特定持久化上下文中进行的改变,也始终立即对在持久化上下文和他的工作单元内部(对实体保证的可重复读取)执行的所有其他代码可见。
85.有两种策略可以在Hibernate或者Java Persistence应用程序中实现对话:利用脱管对象,或者通过扩展一个持久化上下文。
脱管对象状态和已经提到过的重附或者合并的特性是实现对话的方法。用户在思考时间期间对象以脱管状态保存,并且这些对象的任何修改都通过重附或者合并被手工变成持久化。这一策略也被称作利用脱管对象每次请求一个会话。
持久化上下文只跨一个特定请求的处理,兑换期间应用程序手工重附和合并(且有时候脱管)实体实例。
另一种方法不需要手工重附或者合并:利用每次对话一个会话(session-per-conversation)模式,把一个持久化上下文扩展到跨整个工作单元。
Java同一性等价于数据库同一性的条件称作对象同一性的范围(scope of object identity).
对于这一范围,有3中常见的选择:
(1)没有同一性范围的基本持久层不保证一个行是否被访问两次,以及是否会把相同的对象实例返回给应用程序。
(2)持久层利用持久化上下文范围的同一性(persistence context-scoped identity),保证在单个持久化上下文的范围中,只有一个对象实例标示一个特定的数据行。
(3)过程范围的同一性(process-scoped identity)更进一步,保证在整个过程(JVM)中只有一个对象实例表示该行。
86.如果对象引用离开了受保护的同一性范围,那么就称它为脱管对象引用(reference to a detached object)。
假设你想要使用脱管对象,并且必须用自己的子程序测试它们的等同性。可以通过几种方式实现equals()和hashCode()。记住,在覆盖euqals()时,始终也需要覆盖hashCode(),以便这两种方法保持一致。如果两个对象相等,它们就必须有相同的散列码(hashcode)。
一种更好的方法是包括持久化类的所有持久化属性在equals()比较中,远离任何数据库标识符属性。
业务键是一种属性,或者一些属性的组合,它对于每个包含相同的数据库同一性的实例来说是唯一的。本质上,它就是你要使用的自然键,如果你没有正在使用代理主键来代替的话。
业务键是用户作为唯一辨别一个特定记录的东西,而代理键则是引用程序和数据库使用的东西。
业务键等同性意味着equals()方法只比较构成业务键的属性。
87.Hibernate接口
你永远不应该创建一个新的SessionFactory而只是服务一个特定的请求。SessionFactory的创建非常昂贵。另一方面,Session创建则非常便宜。Session甚至知道需要连接时才获得一个JDBC Connection.
get()和load()之间的一个区别在于它们如何表明实例无法被找到。如果数据库中不存在包含给定标识符的行,get()就返回null。load()方法则抛出一个ObjectNotFoundException。由你选择喜欢的错误处理方式。
load()方法可能返回一个代理(proxy),一个占位符,而不命中(hit)数据库。这个结果就是稍后你可能得到一个ObjectNotFoundException,一旦你视图访问返回的占位符,就立即强制它初始化(这也称作延迟加载(lazy loading);load()方法始终视图返回一个代理,如果它以及由当前的持久化上下文管理,则返回一个已经被初始化的对象实例。另一方面,get()方法从不返回代理,它始终命中数据库。
首先,通过给定的标识符从数据库中获取对象。你修改对象,且当tx.commit()被调用时,这些修改在清除期间被传播到数据库。这种机制称作自动脏检查——意味着Hibernate追踪并保存在持久化状态中对一个对象所做的改变。一关闭Session,这个实例就被认为是脱管了。
利用delete()方法可以轻松地是一个持久化对象变成瞬时,从数据库中移除它的持久化状态。
复制采用加载在Session中的脱管对象,并在另一个Session中使他们变成持久化。这些Session通常在已经通过映射给同一个持久化类配置的两个不同的SessionFactiory中打开。
ReplicationMode控制复制过程的细节:
(1)ReplicationMode.IGNORE——当现有的数据库行包含与目标数据库中相同的标识符时忽略对象。
(2)ReplicationMode.OVERWRITE——覆盖任何包含与目标数据库中相同标识符的现有数据行
(3)ReplicationMode.EXCEPTION——如果现有的数据库行包含与目标数据库中相同的标识符时抛出异常。
(4)ReplicationMode.LATEST_VERSION——如果目标数据库的版本比对象的版本更早,则覆盖它里面的行,否则忽略对象。你需要启用Hibernate乐观并发性控制。
87.如果想要报仇对脱管对象所做的修改,必须重附或合并它。
(1)脱管实例可以通过在托管对象上调用update(),被重附到新的Session(并由这个新的持久化上下文托管)。
(2)对lock()的调用把对象与Session和它的持久化上下文关联起来,不强制更新。
(3)不一定要重附托管实例来把它从数据库删除。调用delete()即可。
(4)merge(item)调用导致了几个动作。第一,Hibernate检查持久化上下文中的持久化实例是否具有与正在合并的脱管实例相同的数据库标识符。如果持久化上下文中有相等的持久化实例,Hibernate把脱管实例的状态复制到持久化实例中去。如果持久化上下文中没有相等的持久化实例,Hib就从数据库中加载它(就像用get()所做的那样,通过标识符有效地执行相同的获取),然后把脱管状态与被获取的对象的状态合并。
如果持久化上下文中没有相等的持久化实例,并且在数据库中的查找没有结果,就会创建新的持久化实例,并且把被合并的实例的状态复制到新实例中。然后就计划把这个新对象插入到数据库中,并通过merge()操作返回。
如果传到merge()里面的实例是一个瞬时实例,而不是脱管对象时,也会发生插入。
88.持久化上下文与数据库的同步被称作清除。Hibernate清除发生在以下几个时间点:
(1)当Hibernate API中的Transaction被提交时;
(2)执行查询之前;
(3)应用程序显式地调用Session.flush()时。
可以通过调用session.setFlushMode(),显式地设置Hibernate的FlushMode,来控制这个行为。默认的清除模式为FlushMode.AUTO,并启用前面所说的青蛙。如果你选择了FlushMode.COMMIT,在查询执行之前,持久化上下文不会被清除(只有在手工调用Transaction.commit()或者Session.flush()时,它才会被清除)。这个设置可能让你面临废弃的数据:对只处在内存中的托管对象所做的修改,可能与查询的结果冲突。通过选择FlushMode.MANUAL,可以指定只有显式地调用flush()才导致托管状态与数据库同步。
89.术语非托管是指用Java Persistence创建持久层的可能性,它不用任何特殊的运行环境就可以运行和工作。
与Hibernate SessionFactory相当的是JPA EntityManagerFactory。
对持久化实体实例所做的所有修改都在某个时间点与数据库同步,这个过程称作清除。这种迟写行为与Hibernate的相同,通过尽可能迟地执行SQL DML来保证最佳的可伸缩性。
Hibernate作为一个JPA实现,在下列时间点同步:
(1)当EntityTransaction被提交时
(2)执行查询之前
(3)当应用程序显式地调用em.flush()时。
这些规则与我们在前一节中对原生Hibernate阐述的一样。就像在原生的Hibernate中一样,可以用JPA接口FlushModeType控制这个行为。
对于EntityManager把FlushModeType转换为COMMIT,就会在查询之前禁用自动同步;它只在提交事务或者手工清除时才会发生。默认的FlushModeType是AUTO。
就像原生的Hibernate中使用Session一样,持久化上下文始于createEnetityManager(),止于close()。
托管的运行时环境仿佛是某种容器。应用程序组件就存在这个容器里面。当今的大部分容器都利用一种拦截技术来实现,拦截对象上的方法调用,并应用需要在这个方法之前(或者之后)执行的任何代码。这对任何横切关注点(cross-cutting concern)来说都是最好的:打开和关闭EntityManager是必然的,因为你要它处在被调用的方法里面。
90.数据库把工作单元的概念实现为一个数据库事务(database transaction)。数据库食物组合了数据库访问操作——也就是SQL操作。所有SQL语句都在一个事务内部执行;无法把SQL语句发送到数据库事务之外的数据库。事务被确保以这两种方式终止:要么完全被提交(commit),要么完全被回滚(roll back)。因而,我们说数据库事务是原子的。
一般来说,启动和终止事务的事务范围可以在应用程序代码中编程式地设置,或者声明式地设置。
在一个于几个数据库中操作数据的系统中,特定的工作单元涉及对不止一个资源的访问。既然如此,你就无法单独通过JDBC实现原子性。你需要可以在系统事务(system transation)中处理几个资源的事务管理器(transaction manager)。这样的事务处理系统公开了与开发人员进行交互的Java Transaction API(JTA)。JTA中的主API是UserTransaction接口,包含begin()和commit()系统事务的方法。
让我们来概括一下这些接口,以及什么时候使用它们:
(1)java.sql.Connection——利用setAutoCommit(flase)、commit()和rollback()进行简单的JDBC事务划分。它可以但不应该被用在Hibernate应用程序中,因为它把应用程序绑定到了一个简单的JDBC环境。
(2)org.hibernate.Transaction——Hibernate应用程序中统一的事务划分。它适用于非托管的简单JDBC环境,也适用于以JTA作为底层系统事务服务的应用程序服务器。但是,它最主要的好处在于持久化上下文管理的紧密整合——例如,你提交时Session被自动清除。持久化上下文也可以拥有这个事务的范围。如果你无法具备JTA兼容的事务服务,就是用Java SE中的这个API。
(3)javax.transaction.UserTransaction——Java中编程式事务控制的标准接口,它是JTA的一部分。每当你具备JTA兼容的事务服务,并想编程式地控制事务时,它就应该成为你的首选。
(4)javax.persistence.EntityTransation——在使用Java Persistence的Java SE应用程序中,编程式事务控制的标准接口。
91.Hibernate应用程序中的事务
(1)Java SE中的编程式事务
配置Hibernate为你创建一个JDBC连接池。如果你正在利用Transaction API编写Java SE Hibernate应用程序,处理连接池之外,不需要其他的配置设置。
a.hibernate.transaction.factory_class选项默认为org.hibernate.transaction.JDBCTransactionFactory,这是Java SE中Transaction API以及直接的JDBC的正确工厂。
b.可以用自己的TransactionFactory实现扩展和定制Transaction接口
92.从Hibernate3.x开始,Hibernate抛出的所有异常都是unchecked(未检查)的RuntimeException的子类型,它通常在应用程序中单个的位置进行处理。这也使得所有Hibernate模板或者包装API都变得没用了。
Hibernate抛出类型(typed)异常,即帮组你辨别错误的RuntimeException的所有子类型:
(1)最常见的HibernateException是个一般的错误。你必须检查异常消息,或者在通过异常中调用getCause()找出更多原因。
(2)JDBCException是被Hibernate的内部JDBC层抛出的任何异常。这种异常总是由一个特定的SQL语句产生,可以用getSQL()获得这个引起麻烦的语句。JDBC连接(实际上是JDBC驱动器)抛出的内部异常可以通过getSQLException()或者getCause()获得,并且通过getErrorCode()可以得到特定于数据库和特定于供应商的错误代码。
(3)Hibernate包括JDBCException的子类型和一个内部转换器,该转换器视图把数据库驱动抛出器的特定于供应商的错误代码变成一些更有意义的东西。
(4)Hibernate抛出的其他RuntimeException也应该终止事务。应该始终确保捕捉RuntimeException,无论你计划利用任何细粒度的异常处理策略去做什么。
Hibernate抛出的所有异常都是致命的。这意味着你必须回滚数据库事务,并关闭当前的Session。不允许你继续使用抛出异常的Session。
93.javax.transaction.UserTransaction是启动和终止事务的主要接口。
Hibernate给你正在使用的每个Session获得一个托管的数据库连接,并且还是努力尽可能地延迟。没有JTA,Hibernate将从一开始就停在一个特定的数据库连接上,直到事务终止。有了JTA配置,Hibernate甚至更为积极:获得一个连接,并只用于单个SQL语句,然后立即被返回到托管的连接池。应用服务器保证当另一个SQL语句再次需要连接时,它将在同一个事务期间分发出同一个连接。这个积极的连接——释放模式是Hibernate的内部行文,对于应用程序以及如何编写代码,都没有任何影响。
JTA系统支持全局的事务超时,它可以监控事务。因此,现在setTimeout()控制全局的JTA超时设置——相当于调用UserTransaction.setTransactionTimeout()。
94容器托管事务
声明式事务划分意味着容器替你负责这个关注点。你声明想要代码在一个事务中是否参与以及如何参与。通过应用程序部署人员,提供支持声明式事务划分容器的责任再次落到了它所属的地方。
95.使用Java Persistence的事务
描述本地资源(resource-local)的事务应用到由应用程序(编程式地)控制、且不参与全局系统事务的所有事务。它们直接变为你正在处理的资源的本地事务系统。由于你正在使用JDBC数据库,这意味着本地资源的事务变成了JDBC数据库事务。
JPA中本地资源的事务通过EntityTransaction API控制。这个接口不是为可移植性而存在,而是用来启用Java Persistence的特定特性——例如,当提交事务时,底层持久化上下文的清除。
JPA抛出的异常时RuntimeException的子类型。任何异常都使得当期的持久化上下文无效,并且一旦抛出异常,就不允许你继续使用EntityManager。因此,我们对Hibernate异常处理所讨论的所有策略也适用于Java Persistence异常处理。此外,下列规则也适用:
(1)由EntityManager接口的任何方法抛出的任何异常,都会触发当前事务的自动回滚。
(2)由javax.persistence.Query接口的任何方法抛出的任何异常,都会触发当前事务的自动回滚,处理NoResultException和NonUniqueResultException之外。
注意,JPA不提供细粒度的SQL异常类型。最常见的异常是javax.Persistence.PersistenceException。被抛出的所有其他异常都是PersistenceException的子类型,除了NoResultException和NonUniqueResultException之外,应该把他们全部都当作是致命的。然而,可以在JPA抛出的任何异常中调用getCause(),并找出被包装的原生Hibernate异常,包括细粒度的SQL异常类型。
96.控制并发访问
事务可以在数据库中一个特定的数据项目上放置一把锁,暂时防止通过其他事务访问这个项目。一些现代的数据库通过多版本并发控制(multiversion concurrency control,MVCC)实现事务隔离性这种多版本并发控制通常被认为是更可伸缩的。
1.事务隔离性问题
如果两个事务都更新一个行,然后第二个事务异常终止,就会发生丢失更新(lost update),导致两处变化都丢失。这发生在没有实现锁的系统中。此时没有隔离并发事务。
如果一个事务读取由另一个还没有被提交的事务进行的改变,就发生脏读取(dirty read)。这很危险,因为由其他事务进行的改变随后可能回滚,并且第一个事务可能编写无效的数据。
如果一个事务读取一个行两次,并且每次读取不同的状态,就会发生不可重复读取(unrepeatable read).
不可重复读取的一个特殊案例是二次丢失更新问题(second lost update problem)。想象两个并发事务都读取一个行:一个写到行并提交,然后第二个也写到行并提交。由第一个事务所做的改变丢失了。
幻读(phantom read)发生在一个事务执行一个查询两次,并且第二个结果集包括第一个结果集中不可见的行,或者包括已经删除的行时。(不需要时完全相同的查询)。这种情形是由另一个事务在两次查询执行之间插入或删除行造成的。
2.ANSI事务隔离性级别
标准的隔离性级别由ANSISQL标准定义,但是它们不是SQL数据库特有的。JTA也定义了完全相同的隔离性级别。
(1)允许脏读取但不允许丢失更新的系统,据说要在读取未提交(read uncommitted)的隔离性中操作。如果一个未提交事务以及写到一个行,另一个事务就不可能再写到这个行。但任何事务都可以读取任何行。这个隔离性级别可以在数据库管理系统中通过专门的写锁来实现。
(2)允许不可重复读但不允许脏读取的系统。据说要实现读取提交(read committed)的事务隔离性。这可用共享的读锁和专门的写锁来实现。读取事务不会阻塞其他事务访问行。但是未提交的写事务阻塞了所有其他的事务访问该行。
(3)在可重复读取(repeatable read)隔离性模式中操作的系统既不允许不可重复读取,也不允许脏读取。幻读可能发生。读取事务阻塞写事务(但不阻塞其他的读取事务),并且写事务阻塞所有其他的事务。
(4)可序列化(serializable)提供最严格的事务隔离性。这个隔离性级别模拟连续的事务执行,好像事务是连续地一个接一个地执行,而不是并发地执行。序列化不可能只用低级锁实现。一定有一些其他的机制,防止新插入的行变成对于已经执行会返回行的查询事务可见。
3.选择隔离性级别
4.设置隔离性级别
与数据库的每一个JDBC连接都处于DBMS的默认隔离性级别——通常是读取提交或者可重复读取。可以在DBMS配置中改变这个默认值。还可以在应用程序端给JDBC连接设置事务隔离性,通过一个Hibernate配置选项:
hibernate.connection.isolation=4.
Hibernate在启动事务之前,给每一个从连接池中获取的JDBC连接设置这个隔离性级别。对于这个选项的有意义的值如下:
(1)1——读取未提交隔离性
(2)2——读取提交隔离性
(3)3——可重复读取隔离性
(4)4——可序列化隔离性
注意,Hibernate永远不会改变在托管环境中从应用程序服务器提供的数据库连接中获得的连接隔离性级别!可以利用应用程序服务器配置改变默认的隔离级别。
97.乐观并发控制
1.乐观的方法始终假设一起都会很好,并且和稍有冲突的数据修改。在编写数据时,乐观并发控制只在工作单元结束时才出现错误。
对于如何处理对话中这些第二个事务中的丢失更新,你有3种选择:
(1)最晚提交生效(last commit wins)——两个事务提交都成功,且第二次提交覆盖第一个的变化。没有显示错误消息。
(2)最先提交生效(frist commit wins)——对话A的事务被提交,并且在对话B中提交事务的用户得到一条错误消息。用户必须获取新数据来重启对话,并在此利用没有生效的数据完成对话的所有步骤。
(3)合并冲突更新(merge conflicting updates)——第一个修改被提交,并且对话B中的事务在提交时终止,带有一条错误消息。但是失败的对话B用户可以选择性地应用变化,而不是再次在对话中完成所有工作。
如果你没有启用乐观并发控制(默认情况为未启用),应用程序就会用最晚提交生效策略运行。
2.在Hibernate中启用版本控制
Hibernate提供自动的版本控制。每个实体实例都有一个版本,它可以是一个数字或者一个时间戳。当对象被修改时,Hibernate就增加它的版本号,自动比较版本,如果侦测到冲突就抛出异常。因此,你给所有持久化的实体类都添加这个版本属性,来启用乐观锁。
public class Item
{
private int version;
}
也可以添加获取方法;但是不许应用程序修改版本号。XML格式的<version>属性映射必须立即放在标识符属性之后。
版本号只是一个计数值——它没有任何有用的语义值。实体表上额外的列为Hibernate应用程序所用。
一旦你把<version>或者<timestamp>属性添加到持久化类映射,就启用了包含版本的乐观锁。没有其他的转换。
3.版本控制的自动管理
如果你想要禁用对特定值类型属性或者集合的自动增加,就用optimistic-lock="false"属性映射它。inverse属性在这里没有什么区别。甚至如果元素从反向集合中被添加或者移除,反向集合的所有者的版本也会被更新。
4.没有版本号或者时间戳的版本控制
如果你没有版本或者时间戳列,Hibernate仍然能够执行自动的版本控制,但是只对同一个持久化上下文中获取和修改的对象(即相同的Session)。如果你需要乐观锁用于通过脱管对象实现的对话,则必须使用通过脱管对象传输的版本号或者时间戳。
这种可以选择的版本控制实现方法,在获取对象(或者最后一次清除持久化上下文)时,把当前的数据库状态与没有被修改的持久化属性值进行核对。可以在类映射中通过设置optimistic-lock属性来启用这项功能。
5.用Java Persistence版本控制
Java Persistence规范假设并发数据访问通过版本控制被乐观处理。为了给一个特定的实体启用自动版本控制,需要添加一个版本属性或者字段。
在Hibernate中,实体的版本属性可以是任何数字类型,包括基本类型,或者Date或者Calendar类型。JPA规范只把int、Integer、short、Short、long、Long和java.sql.Timestamp当作可移植的版本类型。
由于JPA标准没有涵盖无版本属性的乐观版本控制,因此需要Hibernate扩展,通过对比新旧状态来启用版本控制。
如果只是希望在版本检查期间比较被修改的属性,也可以转换到OptimisticLockType.DIRTY。然后你还需要设置dynamicUpdate属性为true。
98.获得额外的隔离性保证
有几种方法防治不可重复读取,并涉及到一个更高的隔离性级别。
1.显式的悲观锁
不是把所有的数据库事务转换为一个更高的、不可伸缩的隔离性级别,而是在必要时,在Hibernate Session中使用lock()方法获得更强的隔离性保证:
使用LockMode.UPGRADE,给表示Item实例的(多)行,促成了再数据库中保存的悲观锁。现在没有并发事务可以在相同数据中获得锁——也就是说,没有并发事务可以在你的两次读取之间修改数据。
LockMode.UPGRADE导致一个SQL SELECT... FOR UPDATE或者类似的东西,具体取决于数据库方言。一种变形LockMode.YOGRADE_NOWAIT,添加了一个允许查询立即失败的子句。如果没有这个子句,当无法获得锁时,数据库通常会等待,等待的持续时间取决于数据库,就像实际的SQL子句一样。
在Hibernate中,悲观锁的持续时间是单个数据库事务。
Java Persistence出于同样的目的定义了LickModeType.READ,且EntityManager也有一个lock()方法。规范没有要求未被版本控制的实体支持这种锁模式;但Hibernate在所有的实体中都支持它,因为它在数据库中默认为悲观锁。
2.Hibernate锁模式
Hibernate支持下列其他LockMode:
(1)LockMode.NONE——别到数据库中去,除非对象不处于任何高速缓存中。
(2)LockMode.READ——绕过所有高速缓存,并执行版本检查,来验证内存中的对象是否与当前数据库中存在的版本相同。
(3)LockMode.UPGRADE——绕过所有高速缓存,做一个版本检查(如果适用),如果支持的话,就获得数据库级的悲观升级锁,相当于Java Persistence中的LockModeType.READ。如果数据库方言不支持SELECT ... FOR UPDATE 选项,这个模式就透明地退回到LockMode.READ.
(4)LockMode.UPGRADE_NOWAIT——与UPGRADE相同,但如果支持的话,就使用SELECT ... FOR UPDATE NOWAIT。它禁用了等待并发锁释放,因而如果无法获得锁,就立即抛出锁异常。如果数据库SQL方言不支持NOWAIT选项,这个模式就透明地退回到LockMode.UPGRADE。
(5)LockMode.FORCE——在数据库中强制增加对象的版本,来表明它已经被当前事务修改。这相当于Java Persistence中的LockModeType.WRITE。
(6)LockMode.WRITE——当Hibernate已经在当前事务中写到一个行时,就自动获得他。(这是一种内部模式;你不能在应用程序中指定它。)
默认情况下,load()和get()使用LockMode.NONE。LockMode.READ对session.lock()和脱管对象最有用。
3.强制增加版本
如果通过版本控制启用乐观锁,Hibernate会自动增加被修改实体实例的版本。然而,有时你想手工增加实体实例的版本,因为Hibernate不会把你的改变当成一个应该触发版本增加的修改。
用LockMode.FORCE调用lock(),增加一个实体实例的版本:
Session session=getSessionFactory().openSession();
Transaction tx=session.beginTransaction();
User u=(User)session.get(User.class,123);
session.lock(u,LockMode.FORCE);
u.getDefaultBillingDetails().setOwner("John Doe");
tx.commot();
session.close();
现在,任何使用相同User行的并发工作单元都知道这个数据被修改了,即使只有被你任务是整个聚合的一部分的其中一个值被修改了。
利用Java Persistence的同等调用是em.lock(o,LockModeType.WRITE);
99.非事务数据访问
术语非事务的数据访问意味着没有显式的事务范围,没有系统事务,并且数据访问的行为处于自动提交模式。
首先,必须在Hibernate配置中告诉Hibernate运行自动提交的JDBC连接:
<property name="connection.autocommit">true</property>
利用这个设置,当从连接池获得JDBC连接时,Hibernate不在关闭自动提交——如果连接还没有处于该模式,就启用自动提交。
100.传播Hibernate Session
单个持久化上下文不应该用来处理一个特定的操作,而是处理整个事件(它自然可能需要几项操作)。持久化上下文的范围经常与数据库事务的范围相同。这就是总所周知的每个请求一个会话。
在全局共享的SessionFactory中调用getCurrentSession()的所有数据访问代码,都访问相同的当前Session——如果它在相同的线程中被调用的话。当Transaction被提交(或者回滚)时,工作单元结束。如果提交或者回滚事务,Hibernate也清除和关闭当前的Session以及它的持久化上下文。这意味着提交或者回滚之后对getCurrentSession()的调用生成了新的Session和新的持久化上下文。
Hibernate内部把当前的Session绑定到了当前正在运行的Java线程。【在Hibernate社区中,这也就是大家所知的线程本地会话(Threshold Session)模式】。必须在Hibernate配置中通过把hibernate.current_session_context_class属性设置为thread来启用这个绑定。
如果你给JTA配置Hibernate应用程序,就不必启用这个JTA绑定的持久化上下文;getCurrentSession()始终返回一个被界定和绑定到当前JTA系统事务的Session。
注意,不能把Hibernate的Transaction接口与getCurrentSession()特性和JTA一起使用。你需要Session来调用beginTransaction(),但Session必须被绑定到当前的JTA事务上——这个是个鸡和蛋的问题。这再次强调了你应该始终尽可能地使用JTA,而只有在无法使用JTA时才使用Hibernate的Transaction)。
Hibernate Session有一个内部的持久化上下文。通过扩展持久化上下文来跨整个对象,你可以实现一个不涉及脱管对象的对话。这就是大家所知的每个对话一个会话(session-per-conversation)策略。
一旦你在Session中调用flush(),对任何持久化对象所做的修改就立即被清除到数据库。必须通过设置FlushMode>MANUAL来禁用Session的自动清除——你应该在兑换启动和Session打开的时候完成这项工作。
在青春的期间,由于乐观锁和Hibernate的自动版本检测,在并发对话中所做的修改被隔离。如果你知道最后一步即兑换结束时才清除Session,兑换的原子性就得到了保证——如果关闭未清除的Session,就是有效地终止了对话。
Session中的save()方法要求必须返回被保存的实例的新数据库标识符。因此,标识符值必须在调用save()方法的时候生成。
例外的是INSERT发生之后触发的标识符生成策略。其中一个是identity,另一个是select;两者都要求先插入一个行。如果通过这些标识符生成器映射持久化类,调用save()时就立即执行INSERT!因为你正在对话期间提交数据库事务,这个插入可能有永久的效果。
除了关闭未被清除的持久化上下文之外,一种解决方案是使用补偿动作,执行它们用来撤销在被终止的对话期间所做的任何可能的插入。你必须手工删除插入的行。另一种解决方案是用不同的标识符生成器,如sequence,支持新标识符的生成而不用插入。
persist()方法可以延迟插入,因为它不必返回标识符值。
“每个对话一个会话”策略必须做什么:
(1)当对话启动时,必须用ManageSessionContext.bind()打开和绑定一个新的Session,来服务对话中的第一个请求。你还必须在这个新的Session中设置FlushMode.MANUAL,因为你不想再背后发生任何持久化上下文的同步。
(2)现在调用SessionFactory.getCurrentSession()的所有数据访问代码都接收到了你所绑定的Session。
(3)当对话中的请求完成时,你需要调用ManagedSessionContext.unbind(),并在某处保存现在断开连接的Session,直到对话中有了下一个请求。或者,如果这是对话中的最后一个请求,你需要清除和关闭Session。