- 检索即查询。
- 为了减轻DB的访问压力,提高检索效率,Hibernate对检索进行了优化。
- 所谓检索优化,指的是对查询语句的执行时机进行了细致、严格的把控:并不是代码中一出现查询语句,马上就在后台调用执行select语句。而是在代码中真正需要时才执行select。即将select的执行进行了最大可能的“延迟”。
- 对对象进行检索的目的是为了将对象加载到内存,让程序使用其数据。所以,对象检索也称之为对象加载。直接通过get()、load()等查询语句加载的对象,称之为主加载对象,而主加载对象所关联的对象,称之为关联加载对象或从加载对象。
- 根据检索对象的不同,可以将检索优化分为两类:
1、当前对象检索优化;
2、关联对象检索优化; - 对于不使用优化进行对象检索的过程,称之为直接加载;否则称之为延迟加载,或者懒加载。版本以后是由javassist代理实现的。若实体使用final修饰,将无法生成CGLIB代理,即对于3.3版本之前的Hibernate将无法实现延迟加载。考虑到老版本代码的兼容问题,实体类最好不要使用final修饰。
1 当前对象检索优化
- 对于当前对象进行检索,即对主加载对象进行检索,通常使用Session提供的两个方法:get()和load()。默认情况下,get()为直接加载,而load()为延迟加载。
- get()方法的直接加载指的是,当代码中出现get()时,后台会马上调用执行select语句,将对象直接加载到内存。
-
load()方法的延迟加载指的是,当代码中出现load()时,后台并不会马上调用执行select。只有当代码中真正要访问除了对象的主键id属性以外的其他属性时,即真正要访问对象的详情时,才会真正执行select语句,即此时才会将对象真正加载到内存中。
1.1 get()方法执行代码运行分析
public class Student { private Integer id; private String name; private int age; //setter and getter() public Student(String name, int age) { super(); this.name = name; this.age = age; } public Student() { super(); // TODO Auto-generated constructor stub } @Override public String toString() { return "Student [id=" + id + ", name=" + name + ", age=" + age + "]"; } }
Student student = new Student("aaa", 21); session.save(student);
//在此处加入断点 Student student = session.get(Student.class, 1); System.out.println("id = " + student.getId()); System.out.println("name = " + student.getName());
- 运行结果:对Student对象的加载为立即加载:代码运行到get()时,后台立即调用执行select。未对检索进行优化。
1.2 load()方法执行代码运行分析
- 与get()方法执行条件相同。
//在此处加入断点 Student student = session.load(Student.class, 1); System.out.println("id = " + student.getId()); System.out.println("name = " + student.getName());
- 运行结果:对Student对象的加载为延迟加载:代码运行到load()时,后台并未立即调用执行select;当运行到输出id时,也还未执行select;当运行到输出name是,才进行对student对象的检索加载。将查询的执行推迟到需要真正的数据时,这就是对检索的优化。
- 过程分析:
1、为什么对id属性的访问不会引发select的执行呢? - 因为对于load()方法,第二个参数必须为要加载对象的id,此值不用从DB中获取,直接从load()参数集合获取。所以,对id属性的访问,不会引发select的执行。
2、当执行完load()后,系统都做了些什么? - 当执行完load()后,在Debug调试视图的Variables窗口中可以看到对象的创建过程。系统会生成Student对象的代理对象,该代理对象进行了字节码增强。
- 从该代理对象的属性handler的值可以看出两点:
- Javassist名称部分,说明其进行字节码增强。在Hibernate的Jar包中,required目录下的 javassist-3.18.1-GA.jar 是Java字节码操作助手,可完成字节码增强。
- LazyInitializer名称部分,说明其采用了延迟加载技术。
- 当执行完load()后,handler属性的initialized属性值为false,说明该代理对象尚未被初始化。
- 当执行完对id的输出后,会立即输出id的值,此时,还未执行select。
- 当执行对name输出时,就需要从DB中查询。此时才会真正调用执行select,然后输出name的值,此时,再查看hanlder的initialized属性值,已经由false变为true。
1.3 load()方法默认延迟加载的修改
- load()方法默认情况下采用延迟加载策略,但是也是可以改变的,可以改为直接加载。
- 在该类映射文件的
标签中有个属性lazy,其默认值为true,即采用延迟加载策略。将其值修改为false,load()的执行也将采用直接加载。
1.4 get()与load()的另一个区别
- 当要检索的对象在DB中不存在时,get()方法返回值为null,而load()会抛出异常。
2 关联对象检索优化
- 对于关联对象的检索,即对于从加载对象的检索,也可进行延迟加载优化,采用何种优化策略,要依据映射文件的配置。映射文件中对于关联对象检索的优化配置属性有两个:lazy、fetch。这两个属性是对关联对象的检索进行优化,所以他们是设置在映射文件的关联属性映射中的。
- lazy和fetch各具有若干值,他们不同值的组合,表示不同的对象加载策略。根据这两个属性配置位置的不同,分为两种:
1、多端加载优化;
2、单端加载优化; - 以下均以Country、Minister的1:n双向关联关系为例。
2.1 多端加载优化
- 举例:multipleEndedLoad
- 所谓多端关联加载优化,是指一方为主加载对象,而多方作为从加载对象,对于多方加载时所进行的延迟加载优化配置。鉴于此,fetch、lazy应设置在一方映射文件的关联属性中,即设置在集合
标签中。 - lazy用于指定对象加载时机,即何时加载问题,其取值有:false、true、extra。
- fetch用于指定加载方式,即如何加载问题,即采用哪种select查询,此时其取值有join、select、subselect。
-
以下对于多端关联的加载优化测试,以1:n双向关联为为例,测试类均为以下代码,但对于一方Country的映射文件
标签的属性设置略有不同: @Test public void test() { try { session.beginTransaction(); //在此处加入断点 Country country = session.get(Country.class, 1); //获取当前对象的的集合属性 Set
ministers = country.getMinisters(); //获取集合大小 System.out.println("ministers.size" + ministers.size()); //获取集合详情 for(Minister minister : ministers) { System.out.println(minister); } session.getTransaction().commit(); } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } } 2.1.1 情况1:fetch="join"
- 该加载策略,lazy属性失效,其作用是采用“迫切左外连接"的select语句进行查询。该策略只会生成一条select语句,会将主表与从表进行迫切左外连接(左外连接:主表中的所有记录连接上从表符合条件的部分记录),查询出所有两张表的详细信息。
1、运行条件:
2、运行结果 - 对Country对象执行get()后,会一次性将Country对象及其关联的对象全部查询出来。
3、过程分析
- 对Country对象执行get()后,ministers集合就已经被赋过值,查看其变量的值可看到initialized为true,initializing为false,说明初始化完毕。
2.1.2 情况2:fetch="select" laze="false"
- 该加载会产生多条select语句,并且采用直接加载策略。
1、运行条件:
2、运行结果: - 对Country对象执行get()后,会一次性执行两条select语句:一个是查询出指定的Country对象,然后根据查询出的Country对象的id,再查询出其关联的所有Minister对象。
3、过程分析: - 对Country对象执行get()后,ministers集合就已经被赋过值了,查看其变量的值可看到initialized为true,initializing为false,说明初始化完毕。
2.1.3 fetch="select" lazy="true"
- 该加载策略会产生多条select语句,对于主加载对象,采用其指定的加载策略,但对于关联对象的加载,则采用延迟加载。
1、运行条件:
2、运行结果 - 会先执行一个Country的查询,再进行一个Minister的查询。
3、过程分析 - 对于Country对象执行get()后,会直接加载Country对象,但此时其minister属性尚未初始化。
- 当代码运行至country.getMinisters()时,并不进行对country的关联对象的查询,而是延迟到了ministers.size()的语句运行。这就是对关联对象的延迟加载。
- ministers.size()语句的执行引发对minister的select,对country的ministers属性进行了初始化。
2.1.4 fetch="select" lazy="extra"
- 该加载策略会产生多条select语句。对于主加载对象,采用其指定的加载策略,但是对于关联对象的加载,则采用特别延迟加载。该策略主要针对可能包含聚合函数的检索情况。可以使用聚合函数完成的功能,不进行详情查询。
1、运行条件:
2、运行结果 - 会执行一个对Country的查询:
- 再执行一个聚合函数的检索和minister详情检索:
3、过程分析 - 对Country对象执行get()后,会直接加载Country对象,但此时其ministers属性尚未初始化。注意另一个变量cachedSize的值,现在为-1。
- 当代码运行至country.getMinisters()时,并不进行对country的关联对象的查询。
- ministers.size()语句的执行引发对minister总数的聚合函数的查询select,但并为对country的ministers属性进行初始化。
- 当执行对ministers的输出时,会引发对minister详情的查询,此时ministers被初始化。
2.2 单端加载优化
- 举例:singleEndedLoad
- 所谓单端加载优化,是指多方为主加载对象,而一方作为从加载对象。对于一方加载时所进行的延迟加载优化配置。鉴于此,fetch、lazy设置在多方映射文件的关联属性中,即
标签中。 - lazy用于指定对象加载时机,即何时加载问题。此时其取值有:false、proxy、no-proxy。
- fetch用于指定对象加载方式,即如何加载问题,即采用哪种select查询,此时其取值有:join、select。
- 以下对于单端关联的加载优化测试,以1:n双向关联为例,测试类均为以下代码,但对于多方Minister的映射文件中
标签的属性设置略有不同。 //在此加入断点 Minister minister = session.get(Minister.class, 1); //获取关系属性country对象 Country country = minister.getCountry(); //获取country对象的名称 System.out.println(country.getCid()); System.out.println(country.getCname());
2.2.1 情况1:fetch="join"
- 该加载策略,lazy属性失效,其作用是采用“迫切左外连接”的select语句进行查询。该策略只会生成一条select语句,会将主表和从表进行迫切左外连接(左外连接:主表中的所有记录连接上,从表中符合条件的部分记录),查询出所有两张表的详细信息。
1、运行条件:
2、运行结果:对Minister对象执行get()后,会一次性将Minister对象及其关联的Country对象全部查询出来。
3、过程分析: - 对Minister对象执行get()后,其country属性就已经被赋过值,查看其变量的值可看到cname已经有值。
2.2.2 情况2:fetch="select" lazy="false"
- 该加载策略会产生多条select语句,并且采用直接加载策略。
1、运行条件:
2、运行结果 - 对Minister对象执行get()后,会一次性执行两条select语句:一个是查询出指定的Minister对象,然后根据查询出的Minister的外键值,再查询出其关联的所有Country对象。
3、过程分析 - 对Minister对象执行get()后,其country属性就已经被赋过值了,查看其变量的值可看到cname已经存在值。
2.2.3 情况3:fetch=”select" lazy="proxy" lazy="false"
- 该加载策略会产生多条select语句,主加载对象的加载策略,按其设置进行。但关联对象的加载策略,由其映射类中的加载策略决定。
- 注意,代码中的使用的关联对象的实质并非直接查询出的对象,而是字节码增强的代理对象proxy。DB中查询出的对象此时为目标对象target。在代码中真正使用对象的详情数据时,代理对象才从目标对象中获取该值。
1、运行条件
2、运行结果 - 对Minister对象执行get()后,会首先执行一条select语句,对指定的Minister对象查询。由于Country类的映射文件中设置其加载策略为lazy="false",即立即加载。所以,会马上再执行一条select语句,根据查询出的Minister的外键值,对其关联的Country属性进行查询。
3、过程分析 - 对Minister对象执行get()后,其country属性就已经被赋过值了,查看其变量的值可以看到cname已经有值。
2.2.4 fetch="select" lazy="proxy" lazy="true"
- 对Minister对象执行get()后,会首先执行一条select语句,对指定的Minister对象查询。
- 由于Country类的映射文件中设置其加载策略为lazy="true",即延迟加载,所以会在真正使用详情数据即country.getCname()时才执行下一条select语句,查询Country对象。
- 该策略的执行过程比较复杂,使用到了字节码增强代理。
- 当执行完Minister的get()后,并未做对关联对象Country的查询,而是创建了Country的字节码增强代理对象。
2.2.5 情况5:fetch="select" lazy="no-proxy"
- 该策略要求对实体类”编译时进行字节码增强,否则其与lazy="proxy"效果相同。“编译时进行字节码增强”不做研究。