Hibernate异常与会话

Hibernate的事务和并发 Hibernate的事务和并发控制很容易掌握。Hibernate直接使用JDBC连接和JTA资源,不添加任何附加锁定行为。我们强烈推荐你花点时间了解JDBC编程,ANSI SQL查询语言和你使用的数据库系统的事务隔离规范。Hibernate只添加自动版本管理,而不会锁定内存中的对象,也不会改变数据库事务的隔离级别。基本上,使用Hibernate就好像直接使用JDBC(或者JTA/CMT)来访问你的数据库资源。 

除了自动版本管理,针对行级悲观锁定,Hibernate也提供了辅助的API,它使用了SELECT FOR UPDATE的SQL语法。本章后面会讨论这个API。 

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

1.1.Session和事务范围(transaction scopes) 

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

一个Session的对象是轻型的,非线程安全的,对于单个业务进程,单个的工作单元而言,它只被使用一次,然后就丢弃。只有在需要的时 候,Session 才会获取一个JDBC的Connection(或一个Datasource)对象。所以你可以放心的打开和关闭Session,甚至当你并不确定一个特定 的请求是否需要数据访问时,你也可以这样做。(一旦你实现下面提到的使用了请求拦截的模式,这就变得很重要了。 

此外我们还要考虑数据库事务。数据库事务应该尽可能的短,降低数据库锁定造成的资源争用。数据库长事务会导致你的应用程序无法扩展到高的并发负载。 

一个操作单元(Unit of work)的范围是多大?单个的Hibernate Session能跨越多个数据库事务吗?还是一个Session的作用范围对应一个数据库事务的范围?应该何时打开Session,何时关闭 Session?,你又如何划分数据库事务的边界呢? 

1.1.1.操作单元(Unit of work) 

首先,别再用session-per-operation这种反模式了,也就是说,在单个线程中,不要因为一次简单的数据库调用,就打开和关闭一 次 Session!数据库事务也是如此。应用程序中的数据库调用是按照计划好的次序,分组为原子的操作单元。(注意,这也意味着,应用程序中,在单个的 SQL语句发送之后,自动事务提交(auto-commit)模式失效了。这种模式专门为SQL控制台操作设计的。Hibernate禁止立即自动事务提交模式,或者期望应用服务器禁止立即自动事务提交模式。) 

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

真正的挑战在于如何去实现这种模式:不仅Session和事务必须被正确的开始和结束,而且他们也必须能被数据访问操作访问。用拦截器来实现操作 单元的划分,该拦截器在客户端请求达到服务器端的时候开始,在服务器端发送响应(即, ServletFilter)之前结束。我们推荐使用一个ThreadLocal变量,把Session绑定到处理客户端请求的线程上去。这种方式可以让 运行在该线程上的所有程序代码轻松的访问Session(就像访问一个静态变量那样)。你也可以在一个ThreadLocal变量中保持事务上下文环境, 不过这依赖于你所选择的数据库事务划分机制。这种实现模式被称之为ThreadLocal Session和OpenSession in View。你可以很容易的扩展本文前面章节展示的HibernateUtil辅助类来实现这种模式。当然,你必须找到一种实现拦截器的方法,并且可以把拦 截器集成到你的应用环境中。请参考Hibernate网站上面的提示和例子。 

1.1.2.应用程序事务(Application transactions) 

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

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

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

从用户的角度来看,我们把这个操作单元称为应用程序长事务(application transaction)。在你的应用程序中,可以有很多种方法来实现它。 

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

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

自动版本化-Hibernate能够自动进行乐观并发控制,如果在用户思考的过程中发生并发修改冲突,Hibernate能够自动检测到。 

脱管对象(Detached Objects)-如果你决定采用前面已经讨论过的session-per-request模式,所有载入的实例在用户思考的过程中都处于与Session脱离的状态。Hibernate允许你把与Session脱离的对象重新 

关联到Session上,并且对修改进行持久化,这种模式被称为session-per-request-with-detached-objects。自动版本化被用来隔离并发修改。 

长生命周期的Session(Long Session)-Hibernate的 Session可以在数据库事务提交之后和底层的JDBC连接断开,当一个新的客户端请求到来的时候,它又重新连接上底层的JDBC连接。这种模式被称之 为session-per-application-transaction,这种情况可能会造成不必要的 Session和JDBC连接的重新关联。 

自动版本化被用来隔离并发修改。 

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

1.2.2.使用JTA 

如果你的持久层运行在一个应用服务器中(例如,在EJB session beans的后面),Hibernate获取的每个数据源连接将自动成为全局JTA事务的一部分。Hibernate提供了两种策略进行JTA集成。 

如果你使用bean管理事务(BMT),可以通过使用Hibernate的Transaction API来告诉应用服务器启动和结束BMT事务。因此,事务管理代码和在非托管环境下是一样的。 

代码内容 

// BMT idiomSession 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();} 



在CMT 方式下,事务声明是在session bean的部署描述符中,而不需要编程。除非你设置了属性hibernate.transaction.flush_before_completion 和hibernate.transaction.auto_close_session为true,否则你必须自己同步和关闭Session。 Hibernate可 以为你自动同步和关闭Session。你唯一要做的就是当发生异常时进行事务回滚。幸运的是,在一个CMT bean中,事务回滚甚至可以由容器自动进行,因为由session bean方法抛出的未处理的RuntimeException异常可以通知容器设置全局事务回滚。这意味着在CMT中,你完全无需使用Hibernate 的Transaction API 。 
请注意,当你配置Hibernate事务工厂的时候,在一个BMT session bean中,你应该选择org.hibernate.transaction.JTATransactionFactory,在一个CMT session bean中选择org.hibernate.transaction.CMTTransactionFactory。记住,同时也要设置 org.hibernate.transaction.manager_lookup_class。 

如果你使用CMT环境,并且让容器自动同步和关闭session,你可能也希望在你代码的不同部分使用同一个session。一般来说,在一个非 托管环境中,你可以使用一个 ThreadLocal变量来持有这个session,但是单个EJB方法调用可能会在不同的线程中执行(举例来说,一个session bean调用另一个session bean)。如果你不想在应用代码中被传递Session对象实例的问题困扰的话,那么SessionFactory提供的 getCurrentSession()方法就很适合你,该方法返回一个绑定到JTA事务上下文环境中的session实例。这也是把Hibernate 集成到一个应用程序中的最简单的方法!这个“当前的”session总是可以自动同步和自动关闭(不考虑上述的属性设置)。我们的 session/transaction管理代码减少到如下所示: 

代码内容 

// CMT idiomSession sess = factory.getCurrentSession();// do some work... 


换句话来说,在一个托管环境下,你要做的所有的事情就是调用SessionFactory.getCurrentSession(),然后进行你 的数据访问,把其余的工作交给容器来做。事务在你的session bean的部署描述符中以可声明的方式来设置。session的生命周期完全由Hibernate来管理。 

对after_statement 连接释放方式有一个警告。因为JTA规范的一个很愚蠢的限制,Hibernate不可能自动清理任何未关闭的ScrollableResults或者 Iterator,它们是由scroll()或iterate()产生的。你must通过在finally块中,显式调用 ScrollableResults.close()或者Hibernate.close(Iterator)方法来释放底层数据库游标。(当然,大部分程序完全可以很容易的避免在CMT代码中出现scroll()或iterate()。) 

1.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-不属于任何其他种类的原生异常 

1.3.乐观并发控制(Optimistic concurrency control) 

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

1.3.1.应用程序级别的版本检查(Application version checking) 

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

// foo is an instance loaded by a previous Sessionsession = factory.openSession();Transaction t = session.beginTransaction();int oldVersion = foo.getVersion();session.load( foo, foo.getKey() ); // load the current stateif ( oldVersion!=foo.getVersion ) throw new StaleObjectStateException();foo.setProperty("bar");t.commit();session.close(); 


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

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

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

1.3.2.长生命周期session和自动版本化 

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

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

代码内容 

// foo is an instance loaded earlier by the Sessionsession.reconnect(); // Obtain a new JDBC connectionTransaction t = session.beginTransaction();foo.setProperty("bar");t.commit(); // End database transaction, flushing the change and checking the versionsession.disconnect(); // Return JDBC connection 



foo 对象始终和载入它的Session相关联。Session.reconnect()获取一个新的数据库连接(或者你可以提供一个),并且继续当前的 session。Session.disconnect()方法把session与JDBC连接断开,把数据库连接返回到连接池(除非是你自己提供的数据 库连接)。在Session重新连接上数据库连接之后,你可以对任何可能被其他事务更新过的对象调用Session.lock(),设置 LockMode.READ锁定模式,这样你就可以对那些你不准备更新的数据进行强制版本检查。此外,你并不需要锁定那些你准备更新的数据。假若对 disconnect()和reconnect()的显式调用发生得太频繁了,你可以使用 hibernate.connection.release_mode来代替。 
如果在用户思考的过程中,Session因为太大了而不能保存,那么这种模式是有问题的。举例来说,一个HttpSession应该尽可能的小。 由于Session是一级缓存,并且保持了所有被载入过的对象,因此我们只应该在那些少量的request/response情况下使用这种策略。而且在 这种情况下,Session里面很快就会有脏数据出现,因此请牢牢记住这一建议。 

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

1.3.3.脱管对象(deatched object)和自动版本化 

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

代码内容 

// foo is an instance loaded by a previous Sessionfoo.setProperty("bar");session = factory.openSession();Transaction t = session.beginTransaction();session.saveOrUpdate(foo); // Use merge() if "foo" might have been loaded alreadyt.commit();session.close(); 



Hibernate会再一次在同步的时候检查对象实例的版本,如果发生更新冲突,就抛出异常。 
如果你确信对象没有被修改过,你也可以调用lock()来设置LockMode.READ(绕过所有的缓存,执行版本检查),从而取代update()操作。 

1.3.4.定制自动版本化行为 

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

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

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

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

1.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将使用适当的替代模式(而不是扔出异常)。这一点可以确保应用程序的可移植性

感谢:http://www.cnblogs.com/lightning_wang/archive/2009/12/02/1615676.html

猜你喜欢

转载自justsee.iteye.com/blog/1071781