公司现在有一个比较旧的SSH项目,使用hibernate作为数据访问层框架。因为表之间的关系比较复杂,所以在类与类之间配置了很多的一对多,多对多关联关系,在查数据的时候会查出很多不需要的数据,导致了查询性能的低下,一次简单的分页查询却要耗时一秒以上。
不过,Hibernate也提供了一些方式来提高多表查询性能:可以通过查询策略来优化生成的sql语句,也可以通过缓存来减少sql的生成。下面就介绍下我对缓存及查询策略的一些测试与研究。
Hibernate缓存及查询策略
缓存策略
Hibernate在查询数据时,会根据自身的缓存管理策略,在缓存中查找相关数据。如果发现所需的数据,则直接将此数据作为结果加以使用,从而避免数据库调用性能的开销。
Hibernate缓存包括两大类:一级缓存和二级缓存
Hibernate一级缓存又被称为“Session的缓存”。Session缓存是内置的,不能被卸载,是事务范围的缓存。
Hibernate二级缓存又称为“SessionFactory的缓存”,由于SessionFactory对象的生命周期和应用程序的整个过程对应。第二级缓存是可选的,是一个可配置的插件,默认下SessionFactory不会启用这个插件。
在这里,我只介绍缓存是如何工作的,因为理解了缓存的运作原理才能最大化地利用缓存。具体使用方法请自行百度。
一级缓存
一级缓存默认开启,只要session没有关闭,它就会一直存在。
- 当我们通过hibernate中的session提供的一些API例如 save get update等进行操作时,就会以ID为key将对象保存到session中。当下一次去查询相同ID对象时,就不会去从数据库查询,而是直接从缓存中获取。
二级缓存
Hibernate的二级缓存同一级缓存一样,也是针对对象ID来进行缓存,但需要手动配置。
- 在执行各种条件查询时,如果所获得的结果集为实体对象的集合,那么就会把所有的数据对象分别根据ID放入到二级缓存中。
- 当Hibernate根据ID访问数据对象的时候,首先会从Session一级缓存中查找,如果查不到并且配置了二级缓存,那么会从二级缓存中查找,如果还查不到,就会查询数据库,把结果按照ID放入到缓存中。
- 删除、更新、增加数据的时候,同时更新二级缓存。
查询缓存
在前文中也提到了,Hibernate的一级二级缓存都是通过id对实体进行缓存,但它不会缓存查询结果,如果想对查询结果进行缓存,则可以考虑使用查询缓存。
- 查询缓存是以sql或hql为key,保存查询到的id集合。
- 查询缓存不仅要求所使用的HQL、SQL语句相同,甚至要求所传入的参数也相同,Hibernate才能直接从缓存中取得数据。因此,只有经常使用相同的查询语句、并且使用相同查询参数才能通过查询缓存获得好处,
- 在开启查询缓存的时候,也应该开启二级缓存。如果不使用二级缓存,可能出现N的问题。因为查询缓存只缓存对象的ID或ID集合,二级缓存才是根据ID缓存对象。如果不开启二级缓存,通过查询缓存拿到ID后,还要去发送SQL语句获得对象的信息。
总而言之,查询缓存并不一定能提高应用程序的性能,甚至反而会降低应用性能,因此实际项目中要谨慎的使用查询缓存(后面也会演示这一点)。
还要特别注意一点,对放入缓存中的数据不能有第三方的应用对数据进行更改(其中也包括在自己程序中使用其他方式进行数据的修改,例如,JDBC)。因为那样做Hibernate将不会知道数据已经被修改,也就无法保证缓存中的数据与数据库中数据的一致性。
查询策略
查询策略,指的是Hibernate在查询关联对象时抓取的方式,通过@FetchMode注解来配置,可以设置select,join和subselect。
- select方式时先查询返回要查询的主体对象(列表),再根据关联外键id,每一个对象发一个select查询,获取关联的对象,形成n+1次查询;
- join方式,主体对象和关联对象用一句外键关联的sql同时查询出来,不会形成多次查询。
- subselect是通过两句sql分别查询主体和关联对象,即直接查询主体和通过子查询得到的主体id来查询关联对象
注意,subselect只能设置于多个关联对象。
用一对多来举例:
- fetchMode = "join"是在查询的时候使用外连接进行查询,不会产生1+n的现象
- fetchMode = "select"是在查询的时候先查询出一端的实体,然后在根据一端的查询出多端的实体,会产生1+n条sql语句
- fetchMode = "subselect"是在查询的时候通过两条sql语句查询两端的实体,产生2条sql语句。注意:抓取单个对象不可设置subselect
使用方法(需与@OneToOne/@OneToMany/@ManyToMany一起使用):
@OneToMany
@JoinColumn(name = "entrust_order_id")
@Fetch(FetchMode = "SELECT/JOIN/SUBSELECT")
private List<EntrOrderDetail> entrOrderDetail;;
延时加载
另外,Hibernate提供了延迟加载,也叫懒加载, 即只有真正使用到关联对象的数据时才会获取。
使用方法:
@OneToMany(fetch = FetchType.LAZY/EAGER)
@JoinColumn(name = "entrust_order_id")
private List<EntrOrderDetail> entrOrderDetail;
两个配置项:
lazy 延迟加载
eager 立即加载
特别注意:hibernate的延迟加载在JSP页面中无效,因为延时加载只在hibernate的session中生效,而session在service层提交事务后就关闭了。可以开启openSessioninview延长session的生命周期,达到延时加载的功能。
实际应用
理解了hibernate的查询策略和缓存策略,接下来测试不同配置中,二级缓存/抓取方式/延时加载的效果。
测试思路:控制变量法,通过观察hibernate sql的生成来判断效果。
测试版本:Hibernate 4.3.11
注意,测试篇幅较长,时间宝贵的可以直接看结论
实际使用分为两种情况:
ManyToOne,OneToOne,即获取单个对象
测试实体类:
//关联关系:多个学生对一个老师
//学生
public class Student {
@Id
@GeneratedValue
private Long id;
@Column
private String name;
@ManyToOne
@JoinColumn(name = "teacher_id")
private Teacher teacher; //关联一个老师
}
//老师
//若开启二级缓存:@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Teacher {
@Id
@GeneratedValue
private Long id;
@Column(name = "teacher_name")
private String teacherName;
}
测试代码:
Student student = studentDao.get(1L);
System.out.println("-------测试延时加载------");
System.out.println(student.getTeacher());
测试结果:
- (Hibernate默认设置) fetchMode = “join”,fetchType= eag