Hibernate 系列 05 - Session 类


 

引导目录:

  Hibernate 系列教程 目录

 

前言:

  Session是Hibernate运作的中心,对象的生命周期、事务的管理、数据库的存取都与Session息息相关。

  就如同在编写JDBC时需要关心Connection的管理,以有效的方法创建、利用与回收Connection,以减少资源的消耗,增加系统的执行效能一样,有效的Session的管理也是Hibernate应用时需要关注的焦点。

 

本篇目录:

使用threadLocal变量 Session的缓存

 

1. 使用threadLocal变量

  Session是由SessionFactory创建的,SessionFactory是线程安全的(Thread-Safe),可以让多个执行线程同事存取SessionFactory而不会有数据共享的问题。

  然而Session则不是设计为线程安全的,所以试图让多个执行线程共享一个Session,将会发生数据共享而发生混乱的问题。

 

  在Hibernate参考手册中的第1章快速入门中,示范了一个HibernateUtil,它使用了ThreadLocal类来建立一个Session管理的辅助类,这是Hiberate的Session管理的一个广为应用的解决方案。

  使用ThreadLocal可以有效隔离执行所使用的数据,所以避开了Session的多线程之间的数据共享问题。

 

  以下列出Hibernate参考手册中的HibernateUtil类:

 1 import org.hibernate.*;
 2 import org.hibernate.cfg.*;
 3 
 4 public class HibernateUtil {
 5     private static Log log = LogFactory.getLog(HibernateUtil.class);
 6     private static final SessionFactory sessionFactory;
 7     static {
 8         try {
 9             // 创建 SessionFactory
10             sessionFactory = new Configuration().configure().buildSessionFactory();
11         }catch (Throwable ex){
12             // 当SessionFactory被耗尽时,记录例外
13             log.error("Initial SessionFactory creation falied. ", ex);
14             throw new ExceptionInInitializerError(ex);
15         }
16     }
17     // 下一行的thread_var原本是Session,但Session字面上没有thread_var好理解
18     private static final ThreadLocal thread_var = new ThreadLocal();
19 
20     public static Session currentSession(){
21         Session s = (Session)thread_var.get();
22         // 如果这个线程仍然为空,则打开一个新的Session
23         if(s == null){
24             s = sessionFactory.openSession();
25             thread_var.set(s);
26         }
27         return s;
28     }
29 
30     public static void closeSession(){
31         Session s = (Session)thread_var.get();
32         if (s != null)
33             s.close();
34         thread_var.set(null);
35     }
36 }

 

  使用 ThreadLocal thread_var = new ThreadLocal() 语句生成的thread_var变量是一个只在当前线程有效的变量,也就是说不同线程所拥有的thread_var变量是不一样的。

  在这个变量内可以使用set(object)方法放置保存一个对象,只要这个线程没有结束,都可以通过thread_var变量的get()方法取出原先放入的对象。

  如图所示:

  

 

  对于Session的运用,应该是每一个请求使用一个单独的Session。

 

  例如,修改学生资料,第一个Session取得学生信息,并将其在网页的表单中显示(假设是一个Web应用),然后Session关闭;

  这时打开第二个Session,把修改号的资料同步到数据库中,之后第二个Session关闭。

  整个过程如图所示:

  

 

  如果只是打开一个Session,取得数据,然后等待资料管理员在网页中修改完资料后再提交,得到新数据后进行更新,这样的话,这个Session就占据了太多时间和太多资源。

 

注意:

  Hibernate中的Session与Http请求的Session不一样,Http中的Session表示的是用户向服务器发送请求的一个会话,而Hibernate中Session表示对数据库操作的对象。 

 

  Session类的API操作可以实现具体的数据增加、删除和修改等,关于这些操作的详细内容咱们后面再慢慢讨论。

 

 

2. Session的缓存

  Hibernate中的缓存分为两种:一级缓存(Session级别)和二级缓存(SessionFactory级别)。

  关于二级缓存使我们后面详细讨论的内容,本篇内容咱们重点探讨一级缓存的用法。

 

  每一个Session实例都可以看作为一个容器,无论何时,当给save()、update()或saveOrUpdate()方法传递一个对象时,或使用load()、get()、list()、iterate()或scroll()方法获得一个对象时,该对象豆浆被加入到Session的内部缓存中。

 

  每一个处于持久化状态的对象,当应用程序需要使用对象时,先在本Session的缓存内查找,如果有此对象,则直接返回给应用程序;

  如果没有,则发送SQL语句到数据库中查询,将记录的字段值组装成对象后存于Session中,以供应用程序调用。

 

  简单地说,Session有如下两个作用:

  1) 充当蓄水池的作用,减少程序访问数据库的次数。

      很多对象数据不是经常改变的,第一次访问这些对象时,Hibernate将它放入缓存中,以后只要这个对象没有改动过,访问这个对象时,Hibernate就不回去数据库中加载它的数据,而是从内存中直接返回应用程序。

      这样的效率当然比每次都访问数据库要高得多。

  2) 保证缓存中的数据与数据库同步。

      缓存毕竟不是数据库,它的数据有可能和数据库不一致,这时,Hibernate会负责将缓存中的数据同步到数据库。

      当然,具体的情况要看程序员设置的FlushMode是什么。

 

  一个事务中,对数据进行了操作使其改变了值,这种变化不会立即传递到数据库中。

  Hibernate能把一些数据改变做一下结合,并对数据库进行最小数量的请求,例如少产生几句SQL语句,也可以减轻网络的负担。

 

  例如,一个对象的某个属性发生了两次变化,但是Hibernate指向数据库发送一条update语句就可以把这些数据变化都包括了,这些都归功于Session的缓存。

 

  清理缓存是指查看缓存中的数据与数据库是否同步,如果缓存数据与数据库不一样,则发送更新语句把缓存数据和数据库同步;如果一样,则不作操作。

  在一个Session中,有两种Session的清理方法,也可以说是Session的两个不同清理点,如下代码所示:

1 // 省略代码:打开Session,开启事务...
2 // code segment1
3 // code segment2
4 // 清理点1 - 清理缓存,这种清理经常是在查询语句之前改变了对象的属性值(AUTO, ALWAYS)
5 // code segment3
6 // 清理点2 - 提交事务时,Hibernate调用flush()方法清理缓存(COMMIT)
7 // 省略代码:提交事务,关闭Session...

 

 

  在下面的情况中,Hibernate会调用Session.flush()以清理缓存:

事务提交时,如果flush模式不为FlushMode.MANUAL,commit()将调用flush() 在某些查询语句之前(此查询语句之前的语句已经改变了数据库状态,所以需要调用flush()方法以同步数据,使查询出来的数据使经过更改的) 当程序强制调用Session.flush()时

 

  在调用Session.flush()时,涉及的SQL语句会按照下面的顺序发出执行:

所有对实体进行插入的语句,其顺序按照对象执行Session.save()的时间顺序 所有对实体进行更新的语句 所有进行集合删除的语句 所有对集合元素进行删除、更新或者插入的语句 所有进行集合插入的语句 所有对实体进行删除的语句,其顺序按照对象执行Session.delete()的时间顺序 有一个例外是,如果对象使用native方式来生成ID(持久化标识),则他们一执行save就会被插入

 

  除非明确地指定了flush命令,否则关于Session何时会执行这些JDBC调用是完全无法保证的,只能保证他们执行的前后顺序。

  当然,Hibernate保证Query.list()绝对不会返回已经失效的数据,也不会返回错误数据。

 

  在事务结束时,调用flush()方法以执行事务成功的SQL语句是必需的,因为这样才能和数据库同步。

  在一个事务中调用一个select查询,如果此查询之前已经有某个update语句做了数据修改(注意,此update语句并没有真正执行),或者直接改变了对象的属性,则Hibernate在查询语句执行前首先会调用flush()将缓存中的数据同步数据库,接着才返回查询数据。

  如果select查询之前并没有改变数据的操作,则flush()不会被调用。

 

  通过设置session.setFlushMode(),可以精确地控制Hibernate的FlushMode。

 

  FlushMode有以下几种:

FlushMode.AUTO:Hibernate判断对象属性有没有更改,如果被更改过变成了脏数据,则在一个查询语句前将更新此改动,以保证同步数据库;如果对象没有被改动,则在查询语句之前不用更新数据,这是Hibernate的默认清理格式。 FlushMode.COMMIT:在事务结束之前清理Session的缓存,其他任何时候都不清理缓存。这样的设置将有可能使查询出来的数据使脏数据。例如,对对象做的修改存在于内存中,这可能会和从数据库查询出来的结果相冲突。 FlushMode.MANUAL:(以前的版本里是NEVER,现在已被废弃)除非强制调用Session.flush()方法,否则永不清理缓存。这时对数据所做的修改只限于内存,不会同步到数据库中,数据库的数据相当于只读数据。 FlushMode.ALWAYS:在每一个语句前都调用flush()方法进行缓存清理。这种模式经常是不必要并且低效的。

 

  不推荐改变默认配置,除非在极少情况下配置会有性能提升。

  大部分程序都不需要明确地去调用flush(),只有当使用触发器,或把Hibernate和JDBC语句混合使用,直接调用flush()方法才是很有用的。

  下面通过几个例子,加深对上面FlushMode的理解。

 

1) FlushMode.MANUAL

  编写如下程序:

 1 Session session1 = HibernateUtil.currentSession();
 2 Transaction tx = session1.beginTransaction();
 3 session1.setFlushMode(FlushMode.MANUAL);    // 设置清理模式为手动清理
 4 Student stu = (Student)session1.get(Student.class, 12); // 通过学生主键值studentNo=12得到一个Student.class的实例
 5 System.out.println(stu.getStudentName());    // 此时输出结果为:卡特琳娜
 6 stu.setStudentName("卡特");
 7 session1.save(stu); // 保存学生对象
 8 tx.commit();
 9 HibernateUtil.closeSession();
10 
11 Session session2 = HibernateUtil.currentSession();
12 tx = session2.beginTransaction();
13 stu = (Student)session2.get(Student.class, 12); // 再次得到studentNo=12的学生对象
14 System.out.println(stu.getStudentName());    // 此时输出结果为:卡特琳娜
15 tx.commit();
16 HibernateUtil.closeSession();

 

  运行以上的程序,得到的控制台信息如下:

卡特琳娜
卡特琳娜

 

  可以看到,在session1中所做的更改并没有同步到数据库,所以在session2中取得的学生姓名才会是“卡特琳娜”,否则就应该是更改过的“卡特”了。

 

2) FlushMode.COMMIT

  编写如下程序:

 1 Session session = HibernateUtil.currentSession();
 2 Transaction tx = session.beginTransaction();
 3 session.setFlushMode(FlushMode.COMMIT);    // 设置FlushMode
 4 Student stu = (Student)session.get(Student.class, 12); // 通过学生主键值student=12得到一个Student.class的实例
 5 System.out.println(stu.getStudentName());    // 此时输出结果为:卡特琳娜
 6 stu.setStudentName("卡特");
 7 session.save(stu); // 这条语句并不会发送SQL语句到数据库
 8 session.evict(stu); // 这句非常重要,它将stu对象从Session缓存中除去,以保证下一条语句得到的对象时从数据库中新组装出来的对象(否则将继续使用缓存中的对象)
 9 stu = (Student)session.get(Student.class, 12); // 重新获取studentNo=12的Student对象
10 System.out.println(stu.getStudentName());   // 此时输出结果为:卡特琳娜
11 tx.commit();
12 HibernateUtil.closeSession();

 

  可以看到,session.save(stu)并没有把stu对象的变更同步到数据库,甚至程序结束也没有生成一条update语句。

 

  原因就是使用Session.evict(stu)把变更过的stu从Session中除去了,后来再取得的stu是数据库字段值组装的对象,和数据库是一直的,所以在最后提交事务时,Hibernate县调用session.flush()进行缓存清理,而stu此时没有被修改过,所以不用发送update语句到数据库。

 

3) FlushMode.AUTO

 1 Session session = HibernateUtil.currentSession();
 2 Transaction tx = session.beginTransaction();
 3 session.setFlushMode(FlushMode.AUTO);    // 设置FlushMode
 4 Student stu = (Student)session.get(Student.class, 12); // 通过学生主键值studentNo=12得到一个Student.class的实例
 5 System.out.println(stu.getStudentName());    // 此时输出结果为:卡特琳娜
 6 stu.setStudentName("卡特");
 7 stu = (Student)session.createQuery("from Student s where s.studentNo=12").uniqueResult();
 8 System.out.println(stu.getStudentName());   // 此时输出结果为:卡特
 9 tx.commit();
10 HibernateUtil.closeSession();

控制台显示结果为:

1 Hibernate: select student0_.[StudentNo] as StudentN1_1_0_, student0_.[LoginPwd] as LoginPwd2_1_0_, student0_.[StudentName] as StudentN3_1_0_, student0_.[Gender] as Gender4_1_0_, student0_.[Phone] as Phone5_1_0_, student0_.[Address] as Address6_1_0_, student0_.[BornDate] as BornDate7_1_0_, student0_.[Email] as Email8_1_0_, student0_.[IdentityCard] as Identity9_1_0_, student0_.[GradeId] as GradeId10_1_0_ from [Student] student0_ where student0_.[StudentNo]=?
2 卡特琳娜
3 Hibernate: update [Student] set [LoginPwd]=?, [StudentName]=?, [Gender]=?, [Phone]=?, [Address]=?, [BornDate]=?, [Email]=?, [IdentityCard]=?, [GradeId]=? where [StudentNo]=?
4 Hibernate: select student0_.[StudentNo] as StudentN1_1_, student0_.[LoginPwd] as LoginPwd2_1_, student0_.[StudentName] as StudentN3_1_, student0_.[Gender] as Gender4_1_, student0_.[Phone] as Phone5_1_, student0_.[Address] as Address6_1_, student0_.[BornDate] as BornDate7_1_, student0_.[Email] as Email8_1_, student0_.[IdentityCard] as Identity9_1_, student0_.[GradeId] as GradeId10_1_ from [Student] student0_ where student0_.[StudentNo]=12
5 卡特

 

  在使用session.createQuery()得到学生对象时,Hibernate将清理缓存,发现stu已经被修改,于是发送一条update语句更新数据。

  然后session.createQuery()的select语句才得到被更改过的学生数据,于是,程序可以打印出修改过的姓名“卡特”