一、SessionFactory接口
是单个数据库映射关系(ORM)经过编译后的内存镜像。SessionFactory(的实例)作为应用中的一个全局对象(工厂),可以随处打开/创建一个session,用来进行数据库CRUD操作。
SessionFactory的实例是线程安全的,创建和维护SessionFactory实例的代价都很高,这也决定不会频繁地创建它。通常一个应用(数据库)只对应一个SessionFactory(单例),在应用启
动时创建和一次性静态初始化,在应用退出时关闭。SessionFactory对象可在进程或者集群的级别上,为事务直接可重用数据提供二级缓存。
二、Session接口
Hibernate中的Session是应用程序与数据库交互的单线程对象。其底层封装了JDBC的Connection,也就封装了JDBC需要使用statement完成的CRUD操作,同时Session还是一个Transaction工厂,可以进行局部事务管理。
Session对象是轻量级的,也是线程不安全的。打开和关闭Session实例并不会消耗多少资源,因为只有在需要的时候,Session 才会获取一个JDBC的Connection对象。
Session中有一个必选的一级缓存,所有数据库操作都是暂时存储在Session一级缓存中(Hibernate会监视和检查脏数据),只有flush操作之后才会真正写入数据库, 这意味着,如果你让Session打开很长一段时间,或是仅仅载入了过多的数据, Session占用的内存会一直增长,直到抛出OutOfMemoryException异常,在用户会话期间一直保持 Session打开也意味着出现脏数据的可能性很高。
这个问题的一个解决方法是调用clear() 和evict()来管理 Session的缓存,但是如果你需要大批量数据操作的话,最好考虑 使用存储过程。
三. Hibernate事务和并发
Hibernate本身没有任何事务实现,不在内存中添加额外的锁定行为,而是直接使用了JDBC连接和JTA资源的事务控制。
无论是事务隔离级别,事务并发控制,Hibernate都沿用JDBC/JTA的事务控制那一套。
可以像下面这样为Hibernate指定一种底层事务的实现机制,如果不指定,默认会使用JDBC事务
hibernate.transaction.factory_class org.hibernate.transaction.JTATransactionFactory
hibernate.transaction.factory_class org.hibernate.transaction.JDBCTransactionFactory
5.1 上下文相关session
应用程序在许多场景下都需要一种“上下文相关”的session,但是这种“上下文相关”的范围(scope and context)并不好界定,在Hibernate 3以前,只能依靠一些常用工具类(例如HibernateUtil)或者第三方框架(Spring或Pico)提供的基于代理/拦截器的上下文管理机制。
Hibernate 3.0引入了getCurrentSession(),但强制使用JTA事务控制下的上下文相关session。
Hibernate 3.1则引入新的接口CurrentSessionContext和配置参数current_session_context_class,让getCurrentSession()实现了热拔插。
current_session_context_class支持三种设置,(jta, thread, managed)
- org.hibernate.context.internal.JTASessionContext:当前session由JTA事务来跟着和界定,这其实就是3.0只支持JTA session的原版。
- org.hibernate.context.internal.ThreadLocalSessionContext:当前session由执行的线程跟踪
- org.hibernate.context.internal.ManagedSessionContext:当前session由执行的线程跟踪,但你需要用静态方法绑定session(到线程),这种session不再需要显示开启和关闭。
前两种上下文相关的session策略适合“一个session对应一个数据库事务”的编程模式,也适用于“一个session一个请求”模式,这种策略下,Hibernate session的开始和结束完全由数据库事务的生命周期决定。
如果你正在做普通java开发,通常建议使用Hibernate 事务API来隐藏底层的事务系统;如果你使用JTA,则建议使用JTA相关接口来做事务边界界定;而一旦你使用支持EJB的CMT容器开发,事务的边界管理(即boundaries (begin, commit, or rollback database transactions))是声明式管理的,你不需要进行任何事务或者session的操作。
注意openSession()和getCurrentSession()的区别和联系
- 区别
FactorySession接口中定义了两种获取session的方式,即opSession()和getCurrentSession()。这两种方式的区别如下,
1.getCurrentSession获取一个“上下文相关”的session,即在上下文相关的环境中获取的session就是同一个对象。而openSession()每次都是重新创建一个对象。
2.getCurrentSession创建的线程会在事务回滚或事物提交后自动close(),而openSession必须手动关闭。
- 联系
在 SessionFactory 启动的时候, Hibernate 会根据配置创建相应的 CurrentSessionContext ,在 getCurrentSession() 被调用的时候,实际被执行的方法是 CurrentSessionContext.currentSession() 。在 currentSession() 执行时,如果当前 Session 为空, currentSession 会调用 SessionFactory 的 openSession 。所以 getCurrentSession() 对于 Java EE 来说是更好的获取 Session 的方法。
5.2. 事务隔离
对于Hibernate封装的JDBC类型的事务,可以在Hibernate配置文件中配置一种隔离级别,
关于JDBC事务隔离级别,参考JDBC事务机制
hibernate.connection.isolation 4
5.3 悲观锁和乐观锁
- 悲观锁
一个事务更新数据时,其他事务无法查看/修改数据,因此悲观锁适合短事务(例如取出某个值加1)。
悲观锁通常由数据库机制实现(即 select for update,数据库自动加锁), Hibernate使用session.load(Entity.class, 1, LockMode.UPGRADE)的方式实现。如果使用悲观锁,那么lazy(延迟加载)无效
- 乐观锁
乐观锁是在事务提交前都假定没有冲突,只有在提交的时候才去检查,一旦检查到有冲突的时候,会将处理权交给应用程序。
乐观锁适合长事务及高并发访问。
Hibernate乐观锁的实现方式通常是版本号(version)或者时间戳检查方式。
一种是在数据库表中增加一个版本号字段,更新表前取出版本号,对版本号加一,只有当session中保留的版本号比数据库表中的版本号大才能更新,时间戳跟版本号的检查原理一样。
另一种情况是对于老系统,无法再修改数据库表了,这需要对全字段进行检查,原理跟只检查一个版本号字段是一样的。
Hibernate实现基于版本号/时间戳的乐观锁实现时,需要在映射文件中(紧接主键字段)添加版本号字段,
<version column="version_" name="version" />
5.4 二级缓存
Hibernate的session提供了一级缓存,可以通过flush或者evict清除。
Hibernate的二级缓存需要依赖第三方库,可以通过以下方式配置,
hibernate.cache.region.factory_class org.hibernate.cache.internal.EhCacheRegionFactory
5.5. Hibernate短事务(操作单元)
首先是在单个线程中, 不要因为一次简单的数据库调用,就打开和关闭一次Session,即不要使用session-per-operation的模式,而是应该将多个操作作为一组,当作一个操作单元来提交。
在多用户的client/server应用程 序中,最常用的模式是 每个请求一个会话(session-per-request)。 在这种模式下,来自客户端的请求被发送到服务器端(即Hibernate持久化层运行的地方),一 个新的Hibernate Session被打开,并且执行这个操作单元中所有的数据库操作。 一旦操作完成(同时发送到客户端的响应也准备就绪),session被同步,然后关闭。你也可以使用单 个数据库事务来处理客户端请求,在你打开Session之后启动事务,在你关闭 Session之前提交事务。会话和请求之间的关系是一对一的关系,这种模式对 于大多数应用程序来说是很棒的。
真 正的挑战在于如何去实现这种模式:不仅Session和事务必须被正确的开始和结束, 而且他们也必须能被数据访问操作访问。用拦截器来实现操作单元的划分,该拦截器在客户端请求达到服 务器端的时候开始,在服务器端发送响应(即,ServletFilter)之前结束。我们推荐 使用一个ThreadLocal 变量,把 Session绑定到处理客户端请求的线 程上去。这种方式可以让运行在该线程上的所有程序代码轻松的访问Session(就像访问一 个静态变量那样)。你也可以在一个ThreadLocal 变量中保持事务上下文环境,不过这依赖 于你所选择的数据库事 务划分机制。这种实现模式被称之为 ThreadLocal Session和 Open Session in View。你可以很容易的扩展本文前面章节展示的 HibernateUtil 辅助类来实现这种模式。当然,你必须找到一种实现拦截器的方法,并 且可以把拦截器集成到你的应用环境中。请参考Hibernate网站上面的提示和例子。
5.6 应用程序事务(Long conversations)
很多业务处理流程都需 要一系列完整的和用户之间的交互,即用户对数据库的交叉访问。在基于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连接的重新关联。自动版本化被用来隔离并发修改。
5.7.关注对象标识(Considering object identity), 重写实体类equals()方法和 hashCode() 方法
应用程序可能在两个不同的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对象标识和判等行为如何实现的问题。