Hibernate Lazy&Fetch

1. 前言

本节和大家一起聊聊 Hibernate 中的 LazyFetch 的区别,及两者适合的开发场景。通过本节课程的学习,你将了解到:

  • 什么是延迟加载;
  • 延迟加载的意义。

2. 又见 get() 和 load()

Session 对象提供了 2 个方法用来查询 :

  • get() 方法;
  • load()方法。

如果仅以结果为导向,则无法分辨两者的差异性。

两者如同双胞胎,外观虽然差异不大,但其神韵各有千秋。仔细辨别,便能发现属于各自的特征。

真相只有一个,查明真相的手段,也只有一种:让代码回答

2.1 测试 get() 方法

Student stu=null;
try{
    // 打开事务
    transaction = session.beginTransaction();
    //使用get()方法查询学号为1的学生
    stu=(Student)session.get(Student.class, new Integer(1));
    System.out.println("--------------输出学生信息------------------");
    System.out.println(stu.getStuName());
    transaction.commit();       
} catch (Exception e) {
    transaction.rollback();
} finally {
    session.close();
}
System.out.println("***********关闭Session之后******************");
System.out.println(stu.getStuName());

如上测试代码,和上一节课程的 get() 方法测试有区别:

  • 调用 **get()** 方法查询编号为 1 的学生数据,但会在输出学生数据之前先输出一条提示语句,作为标识分割线;
  • 关闭 Session 对象后继续使用查询出来的学生数据。

查看代码运行结果:

select
student0_.stuId as stuId1_0_0_,
student0_.stuName as stuName2_0_0_,
student0_.stuPassword as stuPassw3_0_0_,
student0_.stuPic as stuPic4_0_0_,
student0_.stuSex as stuSex5_0_0_ 
from
Student student0_ 
where
student0_.stuId=?
--------------输出学生信息------------------
Hibernate是老大
***********关闭Session之后******************
Hibernate是老大

结果能说明什么问题呢?
仔细分析输出的日志信息:

  • 调用 get() 方法时,Hibernate 就构建了一条 Sql 语句。说明,调用 get() 方法时,Hibernate 就跑了一趟数据库,并拿到了开发者指定的数据;

  • 关闭 Session 对象后,程序可以继续使用学生数据。说明,通过 get() 方法获得的数据已经保存到程序运行的内存中,不需要再依赖 Session

想说明什么?不想说明什么?只是一个结论。

2.2 测试 load() 方法

把上面测试实例中的 get() 方法换成 load() 方法。

且运行实例:

Student stu=null;
try{
    // 打开事务
    transaction = session.beginTransaction();
    //查询学号为1的学生
    stu=(Student)session.load(Student.class, new Integer(1));
    System.out.println("--------------输出学生信息------------------");
    System.out.println(stu.getStuName());
    transaction.commit();
} catch (Exception e) {
    transaction.rollback();
} finally {
    session.close();
}
System.out.println("***********关闭Session之后******************");
System.out.println(stu.getStuName());

控制台上查看实例使用结果:

--------------输出学生信息------------------
Hibernate: 
select
student0_.stuId as stuId1_0_0_,
student0_.stuName as stuName2_0_0_,
student0_.stuPassword as stuPassw3_0_0_,
student0_.stuPic as stuPic4_0_0_,
student0_.stuSex as stuSex5_0_0_ 
from
Student student0_ 
where
student0_.stuId=?
Hibernate是老大
***********关闭Session之后******************
Hibernate是老大

得到什么结论了吗?

现在开始寻找区别。

不仔细观察,会误判没有什么区别。

而其中有一个很明显的区别就是:

调用 load ( ) 方法时,Hiberante 并没有真正的行动,只有当执行到下面代码时:

System.out.println(stu.getStuName());

Hibernate 才从容不迫地构建 Sql 语句,往数据库跑了一趟,获得数据,再输出数据。

OK!再稍微改动一下测试代码:

//会话对象
Student stu=null;
try {
    // 打开事务
    transaction = session.beginTransaction();
    //查询学号为1的学生
    stu=(Student)session.load(Student.class, new Integer(1));        
    transaction.commit();
} catch (Exception e) {
    transaction.rollback();
} finally {
    session.close();
}
System.out.println("***********关闭Session之后******************");
System.out.println("--------------输出学生信息------------------");
System.out.println(stu.getStuName());

输出学生信息并不是在调用 load( ) 方法之后,而是关闭 Session 对象之后,结果又会怎样?猜得出来吗?

把你心中的猜想和下面的输出结果比较一下。

没想到吧,抛异常啦,抛异常没什么大惊小怪的,异常是为了告诉你错误原因。查看异常信息,从中找出原因:

org.hibernate.LazyInitializationException: could not initialize proxy - no Session
at org.hibernate.proxy.AbstractLazyInitializer.initialize(AbstractLazyInitializer.java:164)
at org.hibernate.proxy.AbstractLazyInitializer.getImplementation(AbstractLazyInitializer.java:285)
at org.hibernate.proxy.pojo.javassist.JavassistLazyInitializer.invoke(JavassistLazyInitializer.java:185)
省略其它若干……

不要望而生畏,都是纸老虎。找出关键词:

could not initialize proxy - no Session

初看字面意思:不能初始化代理,没有 Session 对象。

什么意思?

到了好好解释这个原因的时候:

调用 load( ) 方法时,Hibernate 根本没有如你所期望一样,往数据库跑。但是又不想让你知道它没去,或是怕你担心它是否能完成这份工作。于是 Hibernate 为你提供了一个 代理对象

这里会涉及到代理设计模式!为不影响主题学习,代理设计模式相关内容自己了解一下。

什么是代理对象?

通俗讲就是说外观和开发者所期望的对象一样,但没有实质性数据。
再通俗点,就是一个替身。有其外形,而无内涵。
再通俗讲:哦,你已经明白了。

Hibernate 这是演的哪一出?这不是摆明欺负人吗?

别误会,这是 Hibernate 的善意之举!善意从何谈起呀!别急!

只有当开发者真正需要数据时:

System.out.println(stu.getStuName());

Hibernate 才会构建 Sql 语句,往数据库跑一趟,获得真正的数据。

但是,执行 Sql 语句是一定要在 Session 的生命周期之内,如果:

session.close();

通往数据库的桥梁被拆了。Hibernate 也无能为力,只能以异常的方式告诉你:

no Session!臣妾做不到呀。

测试 get()、load() 方法的输出结果已经表明了两者的差异性:

  • get() 方法言行一致,说出手呀便出手。开发者一调用,便快马加鞭,从数据库中获得数据,保存到学生对象中,只要学生对象在,数据也就在;
  • load() 方法,看起来倒像是说一套,做一套的主。并不会立马行事,而是创建一个学生代理对象,提供和开发者期待的学生数据对象相同的方法接口,不影响开发者调用。只有当开发者真正需要数据时,才会说,好的,我去看一下数据库。

故而,使用 load() 时就需要特别注意,在 Hibernate 取数据库之前,千万别关闭通向数据库的桥梁:Session 对象

Session 家里有 2 个可用于查询的兄弟:

  • get() 是老实人,言行一致。
  • load() 有点小调皮,有时搞点恶作剧,但心思并不坏。如果真正理解它的意图,在特定的环境下,可能会感动到你。

其实两兄弟都很有趣。

3. 延迟加载

延迟加载?不是在聊 get()load() 方法吗,不是聊得好好的嘛!咋的,中场休息呀。

3.1 什么是延迟加载

什么是延迟加载?前面的测试结论已经给出了答案。

使用 Hibernate 获取数据时,有时,Hibernate 并不急着去数据库,而是等到开发者真正需要数据时才会跑一趟数据库。

load() 方法 和 get() 方法的基础区别:

  • load() 支持延迟加载(Lazy);

意思是,别急,你需要时我再去拿数据。如果没有拿到数据,则会抛出异常。

  • get() 方法不支持延迟加载,而是(Fetch),如果没有拿到数据,则返回 null 。

什么时候使用 get(),什么时候使用 load()。只有需求才能告诉你如何权衡,没有绝对的忠告。

3.2 延迟加载的意义

答案很简单:错峰出行,需时索取。

数据库系统的迎接能力终归是有限的。面对同时有很多数据请求时,就会造成拥堵。并不是所有的数据请求会在它的逻辑中立即使用数据。

于是,就可以使用延迟加载技术,暂缓数据请求,真正需要时,或错开数据库系统的访问高峰期后再访问。

图片描述

在真实的企业级项目中,一个业务逻辑往往是借助于多个组件一起协作完成的。

图片描述

Hibernate 作为数据请求框架,充当数据提供者角色,本身并不处理数据。数据的使用延迟到了数据加工组件之中。

于是,Hibernate 用不着立即造访数据库,先给数据加工组件提供一个代理对象,等数据加工组件真正需要数据时再访问数据库也不迟。

延迟加载是 Hibernate 中的性能优化技术,不要误会它是在使什么小心眼。完全是一番好意。

哲学上讲世界是平衡的,一头变轻,另一头就会变重。总能量消耗不变。

延迟加载技术提供了一种性能优化方式(变轻了),但在还没有真正获取数据之前,不能关闭 Session 对象(生命周期延长,变重了)算是平衡制约吧。

4. 小结

本节课聊到了 Hibernate 中一个很重要的概念:延迟加载,是一种性能优化技术。让开发者在真正需要数据的时候才进入到数据库。

Session 提供的 load() 方法支持延迟加载。但是,千万别以为延迟加载仅仅是 load() 方法的专利。

延迟加载是性能优化技术,Hibernate 在设计时,凡是考虑有必要使用的地方都会有延迟加载的身影。