Hibernate本身并不是数据库,它只是一个轻量级的对象-关系数据库映射(object-relational)工具。它的事务交由底层的数据库连接管理,如果数据库连接有JTA的支持,那么在Session中进行的操作将是整个原子性JTA事务的一部分。Hibernate可以看作是添加了面向对象语义的JDBC瘦适配器(thin adapter)。
SessionFactory的创建需要耗费大量资源,它是线程安全(threadsafe)的对象,在应用中它被所有线程共享。而Session的创建耗费资源很少,它不是线程安全的对象,对于一个简单商业过程(business process),它应该只被使用一次,然后被丢弃。举例来说,当Hibernate在基于servlet的应用中,servlet能够以下面的方式得到SessionFactory。
SessionFactory sf = (SessionFactory)getServletContext().getAttribute("my.session.factory");
每次调用SessionFactory的service方法能够生成一个新的Session对象,然后调用Session的flush(),调用commit()提交它的连接,调用close()关闭它,最终丢弃它。(SessionFactory可能被保存在JNDI或者一个静态的单例(Singleton)辅助变量中。)
在无状态的session bean中,可以同样使用类似的方法。bean在setSessionContext()中得到SessionFactory的实例,每个商业方法会生成一个Session对象,调用它的flush()和close(),当然,应用不应该commit()connection. (把它留给JTA.在容器管理的事务中,数据库连接会自动完成事务。)
我们用上述方法使用Hibernate 的Transaction API,对Transaction执行一次commit()会把所有状态同步,把底层的数据库连接提交(对JTA 事务会特殊处理。)
这里需要理解flush()的含义。 flush()将持久化存储与内存中的变化进行同步,但不是将内存的变化与持久化存储进行同步。注意对所有的Hibernate JDBD 连接/事务来说,其隔离级别将施加于所有的Hibernate执行的操作之上!
接下来的几小节将讨论利用版本化的方法来确保事务原子性,这些“高级”方法需要小心使用。
在创建Hibernate会话(Session)时,你应该留意以下的实践(practices):
对于一个数据库连接,不要创建一个以上的Session或Transaction。
在对于一个数据库连接、一个事务使用多个Session时,你尤其需要格外地小心。Session对象会记录下调入数据更新的情况,所以另一个Session对象可能会遇到过时的数据。
Session不是线程安全的。决不要在两个并发的线程中访问同一个Session。一个Session一般只对应一批需要一次性完成的单元操作!
程序可能在两批单元操作中并发访问同一个对象的持久化状态。不管怎样,持久化类的一个实例不可能在两个Session中共享。所以有两种不同的辨别方式:
foo.getId().equals( bar.getId() )
foo==bar
对于依附于某个特定Session的对象,两种辨别方式是等价的。然而,当程序可能在两个不同的session中并发访问“同一个”(持久化辨别)商业对象时,两个实例(对于JVM辨别来说)却可能是“不同”的。
这种方式把关于并发的头疼问题留给了Hibernate和数据库。程序不需要对任何商业对象进行同步,只要程序坚持每个Session一个线程,或者对象辨别的策略(在一个Session重,程序可以安全的使用==来比较对象)。
许多商业过程需要一系列与用户进行交互的过程,数据库访问穿插在这些过程中。对于web和企业应用来说,跨一个用户交互过程的数据事务是不可接受的。
维护各商业事务间的隔离(isolocation)就成为应用层的部分责任,我们把这种过程称为长时间运行的应用事务(application transaction)。单一的应用事务可能跨越多个数据库事务。如果这些数据库事务中只有一个(最后一个)保存了被修改的数据,其他事务只是简单地读数据,则这个应用事务就是原子性的。
唯一满足高并发性以及高可扩展性的方法是使用带有版本化的乐观并发控制。Hibernate为使用乐观并发控制的代码提供了三种可能的方法。
在整个商业过程中使用一个单独的Session实例以及它的持久化实例,这个Session使用带有版本化的乐观锁定机制,来确保多个数据库事务对于应用来说只是一个逻辑上的事务。在等待用户交互时,Session断开与数据库的连接。这个方法从数据库访问方面来看是最有效的,应用不需要关心对自己的版本检查或是重新与不需要序列化(transient)的实例进行关联。
在整个应用事务中,使用单一的Session 实例和它的持久化实例。
Session 使用带有版本化的乐观锁定来保证多个数据库事务对程序来说就如同是单一的逻辑应用事务。在等待用户交互的时候,Session 脱离所有的底层JDBC连接。对于数据库访问来说,这种方法是最高效的。程序自己不需要关心版本检查或者把已经脱离session的实例重新关联到session。
// foo is an instance loaded earlier by the Session session.reconnect(); foo.setProperty("bar"); session.flush(); session.connection().commit(); session.disconnect();
foo对象仍然知道是哪个Session把自己装载的。 只要Session 拥有一个JDBC连接,我们可以把对象的更改提交。
如果我们的 Session 太大,以至于在用户思考的时间内无法保存住,这种模式就会出现问题。比如,HttpSession应该保持尽量小。因为Session也持有(必须的)第一级缓存,包含所有被装载的对象,我们只能在很少的request/response周期中使用这一策略。这种少用是被鼓励的,因为Session 很快就会出现过时的数据。
每个与持久化存储的交互出现在一个新的Session中,在每次与数据库的交互中,使用相同的持久化实例。应用操作那些从其它Session调入的已经脱离session的实例的状态,通过使用Session.update()或者Session.saveOrUpdate()来重新建立与它们的关联。
// foo is an instance loaded by a previous Session foo.setProperty("bar"); session = factory.openSession(); session.saveOrUpdate(foo); session.flush(); session.connection().commit(); session.close();
你也可以调用lock()而非update(),如果你确信对象没有被修改过,可以使用LockMode.READ(进行一次版本检查,而跳过所有的缓存)。
每当一个新的Session中与数据库出现交互的时候,这个session会在操作持久化实例前重新把它们从数据库中装载进来。我们现在所说的方式就是你的应用程序自己使用版本检查来确保应用事务的隔离性。(当然,Hibernate仍会为你更新版本号)。从数据库访问方面来看,这种方法是最没有效率的,与entity EJB方式类似。
// foo is an instance loaded by a previous Session session = factory.openSession(); int oldVersion = foo.getVersion(); session.load( foo, foo.getKey() ); if ( oldVersion!=foo.getVersion ) throw new StaleObjectStateException(); foo.setProperty("bar"); session.flush(); session.connection().commit(); session.close();
当然,如果在低数据并行(low-data-concurrency)的环境中,并不需要版本检查,你仍可以使用这个方法,只需要忽略版本检查。
The first approach described above is to maintain a single Session for a whole business process thats spans user think time. (For example, a servlet might keep a Session in the user's HttpSession.) For performance reasons you should
上面提到的第一种方法是对于对一个用户的一次登录产生的整个商业过程维护一个Session。(举例来说,servlet有可能会在用户的HttpSession中保留一个Session)。为性能考虑,你必须
提交Transaction(或者JDBC连接),然后
(在等待用户操作前,)断开Session与JDBC连接。
Session.disconnect()方法会断开会话与JDBC的连接,把连接返还给连接池(除非是你自己提供这个连接的)。
Session.reconnect()方法会得到一个新的连接(你也可以自己提供一个),重新开始会话。在重新连接后,你可以通过对任何可能被其它事务更新的对象调用Session.lock()方法,来强迫对你没有更新的数据进行版本检查。你不需要对正在更新的数据调用lock()。
这是一个例子:
SessionFactory sessions; List fooList; Bar bar; .... Session s = sessions.openSession(); Transaction tx = null; try { tx = s.beginTransaction(); fooList = s.find( "select foo from eg.Foo foo where foo.Date = current date" // uses db2 date function ); bar = (Bar) s.create(Bar.class); tx.commit(); } catch (Exception e) { if (tx!=null) tx.rollback(); s.close(); throw e; } s.disconnect();
接下来:
s.reconnect(); try { tx = s.beginTransaction(); bar.setFooTable( new HashMap() ); Iterator iter = fooList.iterator(); while ( iter.hasNext() ) { Foo foo = (Foo) iter.next(); s.lock(foo, LockMode.READ); //check that foo isn't stale bar.getFooTable().put( foo.getName(), foo ); } tx.commit(); } catch (Exception e) { if (tx!=null) tx.rollback(); throw e; } finally { s.close(); }
从上面的例子可以看到Transaction和Session之间是多对一的关系。一个Session表示了应用程序与数据库之间的一个对话,Transaction把这个对话分隔成一个个在数据库级别具有原子性的单元。
用户不需要在锁定策略上花费过多时间,通常我们可以对JDBC连接选定一种隔离级别(isolationn level),然后让数据库完成所有的工作。高级用户可能希望得到悲观锁定或者在新的事务开始时重新得到锁。
Hibernate一直都会使用数据库的锁定机制,而不会在内存中锁定对象。
LockMode类定义了Hibernate需要的不同的锁级别。锁由以下的机制得到:
LockMode.WRITE在Hibernate更新或插入一行数据时自动得到。
LockMode.UPGRADE在用户通过SELECT ... FOR UPDATE这样的特定请求得到,需要数据库支持这种语法。
LockMode.UPGRADE_NOWAIT在用户通过SELECT ... FOR UPDATE NOWAIT这样的特定请求在Oracle数据库环境下得到。
LockMode.READ在Hibernate在不断读(Repeatable Read)和序列化(Serializable)的隔离级别下读取数据时得到。也可以通过用户的明确请求重新获得。
LockMode.NONE表示没有锁。所有对象在Transaction结束时会切换到这种锁模式,通过调用update()或者saveOrUpdate()与会话进行关联的对象,开始时也会在这种锁模式。
“明确的用户请求”会以下的几种方式出现:
调用Session.load(),指定一种LockMode。
调用Session.lock()。
调用Query.setLockMode()。
如果在调用Session.load()时指定了UPGRADE或者UPGRADE_NOWAIT,并且请求的对象还没有被会话调入,那么这个对象会以SELECT ... FOR UPDATE的方式调入。如果调用load()在一个已经调入的对象,并且这个对象调入时的锁级别没有请求时来得严格,Hibernate会对这个对象调用lock()。
Session.lock()会执行版本号检查的特定的锁模式是:READ,UPGRADE或者UPGRADE_NOWAIT。(在UPGRADE或者UPGRADE_NOWAIT,SELECT ... FOR UPGRADE使用的情况下。)
如果数据库不支持所请求的锁模式,Hibernate将会选择一种合适的受支持的锁模式替换(而不是抛出一个异常)。这确保了应用具有可移植性。