• 检索即查询。
  • 为了减轻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 + "]";
	}
}
	<class name="Student" table="t_student">
		<id name="id" column="sid">
			<generator class="native"></generator>
		</id>
		<property name="name" column="sname"></property>
		<property name="age" column="sage"></property>
	</class>
			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()方法默认情况下采用延迟加载策略,但是也是可以改变的,可以改为直接加载。
  • 在该类映射文件的<class/>标签中有个属性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应设置在一方映射文件的关联属性中,即设置在集合<set/>标签中。
  • lazy用于指定对象加载时机,即何时加载问题,其取值有:false、true、extra。
  • fetch用于指定加载方式,即如何加载问题,即采用哪种select查询,此时其取值有join、select、subselect。
  • 以下对于多端关联的加载优化测试,以1:n双向关联为为例,测试类均为以下代码,但对于一方Country的映射文件<set/>标签的属性设置略有不同:
@Test
	public void test() {
		try {
			session.beginTransaction();
			
			//在此处加入断点
			Country country = session.get(Country.class, 1);
			//获取当前对象的的集合属性
			Set<Minister> 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设置在多方映射文件的关联属性中,即<many-to-one/>标签中。
  • lazy用于指定对象加载时机,即何时加载问题。此时其取值有:false、proxy、no-proxy。
  • fetch用于指定对象加载方式,即如何加载问题,即采用哪种select查询,此时其取值有:join、select。
  • 以下对于单端关联的加载优化测试,以1:n双向关联为例,测试类均为以下代码,但对于多方Minister的映射文件中<many-to-one/>标签的属性设置略有不同。
	//在此加入断点
	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"

1、运行条件 2、运行结果:

  • 对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"效果相同。“编译时进行字节码增强”不做研究。