下面我们来研究hibernate的三种状态,hibernate的三种状态是hibernate学习的一大难点和重点,只有我们能够很好地掌握hibernate的三种状态,我们才能很好的掌握hibernate中的各种问题。下面的图就是hibernate的三种状态,Transient(瞬时状态)、Persistent(持久化状态)、Detached(离线状态)。
下面我们创建一个单元测试,来深入的研究下hibernate的三种状态。
publicclass TestStatus { @Test publicvoid testTransient() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = new User("ljh","123","刘帅哥", DateUtil.getDate("1990-08-03")); /** * 注意,以上状态的对象u就是Transient(瞬时)状态, * 就是数据库中没有,Session中也没有, * 但是对象存在,这就是瞬时状态 */ session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } } |
什么是瞬时状态,就是Session中没有,数据库中也没有,但是对象已经存在的状态。
由瞬时状态进入持久化状态的方法之一:
package com.lzcc.hibernate.test;
import org.hibernate.Session; import org.junit.Test;
import com.lzcc.hibernate.entity.User; import com.lzcc.hibernate.util.DateUtil; import com.lzcc.hibernate.util.HibernateUtil;
publicclass TestStatus {
@Test publicvoid testTransient() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = new User("ljh","123","刘帅哥", DateUtil.getDate("1990-08-03")); /** * 注意,以上状态的对象u就是Transient(瞬时)状态,就是数据库*中没有,Session中也没有, * 但是对象存在,这就是瞬时状态 */ session.save(u); /** * 注意,一定执行了save方法,则对象由瞬时状态进入Persistent(持*久化)状态,此时Session中存在该对象,数据库中也存在该对象 */ session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } } |
持久化状态就是Session中存在该对象,数据库中也存在,这种状态就是持久化状态。持久化的的状态其实质是在内存中有了一份对对象的拷贝,这个我们后面再说。
我们来运行代码:
代码运行成功。如果此时我们这样来修改代码,那么会发生什么事情呢?
package com.lzcc.hibernate.test;
import org.hibernate.Session; import org.junit.Test;
import com.lzcc.hibernate.entity.User; import com.lzcc.hibernate.util.DateUtil; import com.lzcc.hibernate.util.HibernateUtil;
publicclass TestStatus {
@Test publicvoid testTransient() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = new User("ljh","123","刘帅哥", DateUtil.getDate("1990-08-03")); /** * 注意,以上状态的对象u就是Transient(瞬时)状态,就是数据库中没有,Session中也没有, * 但是对象存在,这就是瞬时状态 */ session.save(u); /** * 注意,一定执行了save方法,则对象由瞬时状态进入Persistent(持久化)状态 */ u.setNickname("刘大帅哥");//注意,此时我没有调用update方法哦 session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } } |
运行结果:
我们发现当对象变成持久化状态后,当我们修改了对象的属性后,即使我们没有调用update方法,hibernate也会执行update方法来完成修改的。
为什么会发生这样的事呢?这是因为只要当hibernate的对象处于持久化的状态时,此时hibernate会将对象的信息会保存到一块缓存中,只要session还没有提交,当session去提交时,hibernate就会自动的判断该对象与缓存中的缓存的对象是否一致,如果一致,这只执行save方法,如果不一致,在执行save方法,会紧接着执行update方法。
下面我们再写个方法来看看:
@Test publicvoid testPersistent01() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = new User("ts","123","唐僧", DateUtil.getDate("535-01-03")); session.save(u); u.setPassword("456"); session.save(u); u.setNickname("唐三藏"); session.update(u); u.setUsername("tsz"); session.update(u); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
我们在这个过程中save了两次,update了两次,我们来看看会发送几条sql语句
我们发现,不管我们在这个过程中save了几次,update了几次,sql语句一直都说两条,为什么呢?
因为当我们save的方法在第一次提交时,u对象已经就处于持久化状态了,当对象处于持久化状态后,针对该对象的save或者update就失去意义,当代码提交的时候,hibernate先会去缓存中判断该对象与缓存中的对象是否一致,如果一致,则执行save保存方法,如果不一致,则在保存后,执行update方法。
我们来测试是否真是这样:
@Test publicvoid testPersistent02() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = new User("ts","123","唐僧", DateUtil.getDate("535-01-03")); session.save(u); session.save(u); session.update(u); session.update(u); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
代码运行效果:
我们发现确实是这样,我们这次没有修改对象,该对象与缓存中的对象一致,则不管我们调用几次save和update语句,hibernate只执行一次save方法,其他的都忽略了。
我们继续研究hibernate的三种状态:
@Test publicvoid testPersistent03() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); //当我们将u从数据库中取出,则u变成了持久化状态了 User u = (User) session.load(User.class, 18); u.setNickname("hahha"); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
如果我们将一个对象从数据库中取出来,该对象则会变成Persistent状态,此时我们修改了该对象,那么会发送几条sql语句呢?
我们发现还是发送了两条sql,这样也就符合了我们前面说的。
一定要记住:当一个对象处于持久化状态后,hibernate会在缓存中保存一份该对象的拷贝,如果说我们修改了该对象,则在提交的时候,hibernate会先去判断缓存中的对象与该对象是否一致,如果不一致,则肯定会发送修改sql去修改对象,使其一致的。
我们现在来看上面的图,在图上我们可以看出不管load、get、find或者其他方法,只要是将对象从离线状态变成了持久化状态后,或者是一个新对象,我们通过save或者updat方法将其变成了持久化状态后,要遵循上面的这段话的。
我们再次测试,看看我们分析的对吗?
@Test publicvoid testPersistent04() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); //当我们将u从数据库中取出,则u变成了持久化状态了 User u = (User) session.load(User.class, 18); //注意,如果我们这儿不答应u的话,不会发送sql,这是因为延迟加载的//问题,这个问题,我们后面再说 System.out.println(u); session.update(u); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
我们发现还是一条sql,这就符合了我们的分析。
下面我们来看离线状态(Detached)
什么是Detached状态呢?就是数据库中有对象,但是没有被Session接管的对象。
@Test publicvoid testDetached01() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); /** * 此时,u这个对象就是出于离线状态,此时u存在,数据库中也存在 * 但是没有被Session接管 */ User u = new User(); u.setId(18); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
那么我们如果将一个对象从离线转换为持久化状态呢?
我们先来看看save方法:
@Test publicvoid testDetached02() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); /** * 此时,u这个对象就是出于离线状态,此时u存在,数据库中也存在 * 但是没有被Session接管 */ User u = new User(); u.setId(18); /** * 注意,如果我们执行了save方法后,此时id就无效了,hibernate会新插入一条数据的 */ session.save(u); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
所以我们知道就行了,我们肯定不会使用save方法将离线转换为持久状态的。
那么update呢?
@Test publicvoid testDetached03() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = new User(); u.setId(18); session.update(u); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
我们发现update后,对象也会从离线状态转换为持久化状态,但是注意,此时该对象的属性值如果不全或者没有,则会修改了数据库中的值的。
@Test publicvoid testDetached04() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = new User(); u.setId(18); session.update(u); u.setBornDate(DateUtil.getDate("1990-01-01")); u.setNickname("刘帅哥"); u.setPassword("123"); u.setUsername("ljh"); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
我们发现还是发生了一条sql,因为前面我们说过了,当update后,对象就会变成持久化对象了,此时,就符合我们前面持久化的说法了,hibernate就在提交的时候回去缓存中判断该对象与缓存中的存的对象是否一致,如果一致就修改,否则不修改。
前面我们看了如果将一个离线状态的对象转换为持久化对象,那么我们如何将一个对象从离线状态转换为瞬时状态呢?对,就是删除,当我们删除了一个对象后,数据库中就没有了该对象。
@Test publicvoid testDetached05() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = new User(); u.setId(18); /** * 当我们删除了u,u就从离线状态进入瞬时状态了 */ session.delete(u); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
@Test publicvoid testDetached06() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = new User(); u.setId(19); /** * 此时u为离线状态,如果我们调用save方法,则会增加一条数据,如果我们调用了update方法 * 则会修改一条数据,所以hibernate为我们提供了一个方法saveOrUpdate方法 */ /** * 如果数据库存在这条数据,则更新,如果不存在,则修改 * 注意:此方法不常用 */ session.saveOrUpdate(u); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
我们来看下面的案例,这个案例就更有意思了
@Test publicvoid testDetached07() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); //u1是持久化状态 User u1 = (User) session.load(User.class, 20); System.out.println(u1);//防止延迟加载问题 //u2是离线状态 User u2 = new User(); u2.setId(20); //注意,u1和u2的id是相同的 session.saveOrUpdate(u2); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
运行代码:
我们发现代码报错了,提示我们说一个对象,在缓存中有了两份对象的拷贝,这是因为在Session的缓存中一个对象只能有一个拷贝,而u1和u2的id相同,hibernate认为它们是一个对象,所以就报错了。
@Test publicvoid testDetached07() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); //u1是持久化状态 User u1 = (User) session.load(User.class, 20); System.out.println(u1);//防止延迟加载问题 //u2是离线状态 User u2 = new User(); u2.setId(20); //注意,u1和u2的id是相同的 //当调用saveOrUpdate方式是u2就变成了持久化状态了,那么在Session的缓存中要保存一份 //但是Session的缓存一个对象只能保存一份,u1和u2的id相同,hibernat认为这是一个对象, //所以保存了 // session.saveOrUpdate(u2); //marge方法会判断session中是否存在一个该对象,如果存在,则合并两个对象 //但是这个方法一般不用啊,知道就行了 session.merge(u2); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
这就是hibernate的三种状态,以及它们之间的转换,大家要记住的就是上面的那张图,还有就是Transient(瞬时)状态就是数据库中没有,Persistent(持久化)状态就是数据库中有,并且被session管理的状态,离线状态(Detached)就是数据库中有,但是没有被session管理。当然还有clear方法,当session被clear后,session就被清空了,此时如果数据库中有,则是离线,如果数据库中没有,则是瞬时。
下面我们继续来看hibernate的延迟加载问题,hibernate创始人在设计hibernate的时候,考虑了大量的性能问题,所以使得hibernate的异常的难用,其他最典型的问题就是延迟加载问题(lazy)。其实我们前面也遇到了延迟加载问题,只是我们没有专门的研究它而已。
创建一个单元测试类,我们来专门的看看延迟加载问题。
package com.lzcc.hibernate.test;
import org.hibernate.Session; import org.junit.Test;
import com.lzcc.hibernate.entity.User; import com.lzcc.hibernate.util.HibernateUtil;
publicclass TestLazy {
@Test publicvoid testLazy01() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = (User) session.load(User.class, 17); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } } |
执行这段代码,我们发现居然没有发送sql语句
这就是延迟加载问题,为什么会出现这个问题呢?
这就是我前面说的,hibernate的创始人在开始设计hibernate的时候考虑了大量的性能问题,为hibernate设计了延迟加载,在这段代码上u被load后没有再使用,hibernate考虑到如果你不使用这个u对象,那么发送sql就没必要了,所以就延迟发送sql语句了,如果我们在这儿使用了u对象的话,就会发送sql:
@Test publicvoid testLazy02() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = (User) session.load(User.class, 17); System.out.println(u);//使用了u session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
下面我们来具体的说下,什么是延迟加载,延迟加载就是当我们load一个对象时,并不会马上发送sql去得到这个对象,只有当我们要使用的时候,才会去发送sql。当load完成后,u其实是一个代理对象,这个代理对象在session中仅仅只有一个id的值。
@Test publicvoid testLazy03() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = (User) session.load(User.class, 17); System.out.println(u.getId()); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
运行代码:
我们发现打印出了17,但是没有发送sql,因为这时候,我们只是使用id,而hibernate认为你仅仅使用id,在代理对象中是有id(我们说了,代理对象中仅仅有一个id),那么它就不会发送sql去数据库中取值,而是使用代理对象的id,所以id的值打印出来了,但是sql没有发送。这个都是为了效率考虑,但是其实好多情况下,这些没有必要(特别是在分层体系中),反倒是增加了hibernate的学习难度。
@Test publicvoid testLazy04() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = (User) session.load(User.class, 17); System.out.println(u.getId()); System.out.println(u.getNickname()); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
此时,要使用nickname了,hibernate发现在代理对象中没有nickname的值,就会发送sql去数据库中取数据,这就是延迟加载。
延迟加载本身的设计初衷是为效率考虑,这个没有问题,但是因为有了延迟加载,所以就会引发一系列的问题,下面我们来看看。
我们先看看get方法有延迟加载吗?
@Test publicvoid testLazy05() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = (User) session.get(User.class, 17); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
运行代码:
get方法不管有没有使用,它都会去数据库中去数据,所以get方法一旦调用,不会延迟加载,直接就发送sql去数据库中取数据。所以get方法没有延迟加载。load方法由延迟加载。
好了,下面我们看看延迟加载给我们带来的问题吧。
@Test publicvoid testLazy06() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = (User) session.get(User.class, 110);//数据库中没哟id为110的数据。 System.out.println(u.getNickname()); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
运行代码,抛出异常,我们来看看是什么异常:
空指针异常,因为id=110的数据不存在,那么查询后该u这个对象为空null,我们去使用空对象的属性,则抛出空指针异常。
那么我们要是使用了load呢?
@Test publicvoid testLazy07() { Session session = null; try { session = HibernateUtil.openSession(); session.beginTransaction(); User u = (User) session.load(User.class, 110);//数据库中没哟id为110的数据。 System.out.println(u.getNickname()); session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { HibernateUtil.closed(session); } } |
我们发现两个方法抛出的异常不一样,load方法抛出的是对象找不到异常,注意:这个是一个考点,面试的时候经常问的。为什么呢?
因为load方法存在延迟加载,在session中有个只用id为110的对象拷贝,当我们使用了nickname是,它方向在缓存中没有,这时候就发送sql去数据库中去查找,结果没有查到,但是u不为空,因为u在缓存中有一个id为110的拷贝,所以抛出的异常就是对象找不到异常了。
我们说了延迟加载会引起一系列的问题,下面我们来看看延迟加载在真实的项目中会引起什么问题。
我们创建一个UserDao:
package com.lzcc.hibernate.dao;
import org.hibernate.Session;
import com.lzcc.hibernate.entity.User; import com.lzcc.hibernate.util.HibernateUtil;
publicclass UserDao {
/** * 通过id得到一个User对象 * @param id用户id * @return User用户对象 */ public User load(int id) { User u = null; Session session = null; try { session = HibernateUtil.openSession(); u = (User) session.load(User.class, id); } catch (Exception e) { e.printStackTrace(); } finally { HibernateUtil.closed(session); } return u; } } |
我们在单元测试时调用该方法:
@Test publicvoid testLazy08() { UserDao dao = new UserDao(); User u = dao.load(17); System.out.println(u); } |
根据我们前面所学的知识,这个调用没有任何问题的,但是我们执行代码后发现报错了。
这个异常时因为延迟加载引起的,注意,在hibernate中,这个异常是非常常见的一个异常,延迟加载初始化异常,没有一个初始化的代理对象,为什么会引起这个异常了,这就是我们的延迟加载引起的,我们在UserDao中使用了Session来load了一个对象,之后我们关闭了session,但我们在其他地方再次调用这个load方法时,此时session已经关闭了,load方法不会直接去发送sql预计完成查询,等页面上我们要使用u对象除id外的属性时,才会去发送sql完成查询,但是此时,session已经关闭了。那么就不能再发送sql了,但是页面要求发送,所以就引起了这个错误。
那么如何解决这个问题呢?
1、 不要使用load方法,如果我们使用get方法,get方法不会延迟加载,所以就不会出现这个问题。
2、 不在dao层关闭session,但是我们知道session使用完了要关闭,那么我们在什么地方关闭session呢?我们在页面使用完了值后在页面关闭session,那么我们如何将session传递到页面了,我们要使用ThreadLocal来传递session对象,ThreadLocal这个对象的特点就是单独的线程运行,保存的值可以在各个层中传递不会丢失,只有我们调用了它的remove方法后才会移除。Spring的OpenSessionInView就是这么实现的,我们现在不解决这个问题,等我们学习了Spring时,我们再来看OpenSessionInView的方案。
我们在前面的时候,使用的id生成时,我建议大家使用native,它会根据数据库的不同选择各自的自动id生成方式,如MySQL,如果我们要让id自增,则应该使用increment,sql service应该使用identity,Oracle则应该使用sequence,其实不管哪种数据库native和上面的三种都是增加的id生成方式。
我们发现,其实除了以上几种还有assigned这种方式,注意:这种方式的id生成规则是:数据库不再指定ID生成,有程序员自己填入id,如果程序员不填,第一次生成为0,第二次也为0,所以第二次就报错了,因为id重复了。
还有就是uuid的生成方式,如果使用了uuid的生成方式,注意:
1、 实体类的id字段必须使用String类型
2、 uuid的生成方式是hibernate借助了UUID这个类的生成方式执行的,所以每次生成的字符串是不一样的,并且保证也不会重复(UUID的特点,我们前面说过的)。
总结:在使用hibernate的ID生成策略上,我们一般只会使用native或者uuid,但是这两者的优缺点都有,所以我们要根据自己的项目实际情况来选择到底使用哪种方式。
uuid的好处就是不重复,缺点就是检索时不方便(肯定没有数字方便)
native的好处就是检索方便,但是缺点就是每次插入时,在插入前都需要先来查询下已经到什么数字了(如已经插入了15条记录了,那么再插入id就是16,那么我们是不是要先知道已经插入了15条,需要先查询下),同时当数据量大道一定量的时候native可能就不能支持了(但是,注意hibernate的ID是long类型,所以这个数据量一定要的海量级别才可能发生)。