在本篇随笔里将会分析一下hibernate的缓存机制,包括一级缓存(session级别)、二级缓存(sessionFactory级别)以及查询缓存,当然还要讨论下我们的N+1的问题。
随笔虽长,但我相信看完的朋友绝对能对hibernate的 N+1问题以及缓存有更深的了解。
一、N+1问题
首先我们来探讨一下N+1的问题,我们先通过一个例子来看一下,什么是N+1问题:
list()获得对象:
1 /** 2 * 此时会发出一条sql,将30个学生全部查询出来 3 */ 4 List<Student> ls = (List<Student>)session.createQuery("from Student") 5 .setFirstResult(0).setMaxResults(30).list(); 6 Iterator<Student> stus = ls.iterator(); 7 for(;stus.hasNext();) 8 { 9 Student stu = (Student)stus.next(); 10 System.out.println(stu.getName()); 11 }
如果通过list()方法来获得对象,毫无疑问,hibernate会发出一条sql语句,将所有的对象查询出来,这点相信大家都能理解
Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.rid as rid2_, student0_.sex as sex2_ from t_student student0_ limit ?
那么,我们再来看看iterator()这种情况
iterator()获得对象
1 /** 2 * 如果使用iterator方法返回列表,对于hibernate而言,它仅仅只是发出取id列表的sql 3 * 在查询相应的具体的某个学生信息时,会发出相应的SQL去取学生信息 4 * 这就是典型的N+1问题 5 * 存在iterator的原因是,有可能会在一个session中查询两次数据,如果使用list每一次都会把所有的对象查询上来 6 * 而是要iterator仅仅只会查询id,此时所有的对象已经存储在一级缓存(session的缓存)中,可以直接获取 7 */ 8 Iterator<Student> stus = (Iterator<Student>)session.createQuery("from Student") 9 .setFirstResult(0).setMaxResults(30).iterate(); 10 for(;stus.hasNext();) 11 { 12 Student stu = (Student)stus.next(); 13 System.out.println(stu.getName()); 14 }
在执行完上述的测试用例后,我们来看看控制台的输出,看会发出多少条 sql 语句:
Hibernate: select student0_.id as col_0_0_ from t_student student0_ limit ? Hibernate: select student0_.id as id2_0_, student0_.name as name2_0_, student0_.rid as rid2_0_, student0_.sex as sex2_0_ from t_student student0_ where student0_.id=? 沈凡 Hibernate: select student0_.id as id2_0_, student0_.name as name2_0_, student0_.rid as rid2_0_, student0_.sex as sex2_0_ from t_student student0_ where student0_.id=? 王志名 Hibernate: select student0_.id as id2_0_, student0_.name as name2_0_, student0_.rid as rid2_0_, student0_.sex as sex2_0_ from t_student student0_ where student0_.id=? 叶敦
.........
我们看到,当如果通过iterator()方法来获得我们对象的时候,hibernate首先会发出1条sql去查询出所有对象的 id 值,当我们如果需要查询到某个对象的具体信息的时候,hibernate此时会根据查询出来的 id 值再发sql语句去从数据库中查询对象的信息,这就是典型的 N+1 的问题。
那么这种 N+1 问题我们如何解决呢,其实我们只需要使用 list() 方法来获得对象即可。但是既然可以通过 list() 我们就不会出现 N+1的问题,那么我们为什么还要保留 iterator()这种形式呢?我们考虑这样一种情况,如果我们需要在一个session当中要两次查询出很多对象,此时我们如果写两条 list()时,hibernate此时会发出两条 sql 语句,而且这两条语句是一样的,但是我们如果第一条语句使用 list(),而第二条语句使用 iterator()的话,此时我们也会发两条sql语句,但是第二条语句只会将查询出对象的id,所以相对应取出所有的对象而已,显然这样可以节省内存,而如果再要获取对象的时候,因为第一条语句已经将对象都查询出来了,此时会将对象保存到session的一级缓存中去,所以再次查询时,就会首先去缓存中查找,如果找到,则不发sql语句了。这里就牵涉到了接下来这个概念:hibernate的一级缓存。
二、一级缓存(session级别)
我们来看看hibernate提供的一级缓存:
1 /** 2 * 此时会发出一条sql,将所有学生全部查询出来,并放到session的一级缓存当中 3 * 当再次查询学生信息时,会首先去缓存中看是否存在,如果不存在,再去数据库中查询 4 * 这就是hibernate的一级缓存(session缓存) 5 */ 6 List<Student> stus = (List<Student>)session.createQuery("from Student") 7 .setFirstResult(0).setMaxResults(30).list(); 8 Student stu = (Student)session.load(Student.class, 1);
我们来看看控制台输出:
Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.rid as rid2_, student0_.sex as sex2_ from t_student student0_ limit ?
我们看到此时hibernate仅仅只会发出一条 sql 语句,因为第一行代码就会将整个的对象查询出来,放到session的一级缓存中去,当我如果需要再次查询学生对象时,此时首先会去缓存中看是否存在该对象,如果存在,则直接从缓存中取出,就不会再发sql了,但是要注意一点:hibernate的一级缓存是session级别的,所以如果session关闭后,缓存就没了,此时就会再次发sql去查数据库。
1 try 2 { 3 session = HibernateUtil.openSession(); 4 5 /** 6 * 此时会发出一条sql,将所有学生全部查询出来,并放到session的一级缓存当中 7 * 当再次查询学生信息时,会首先去缓存中看是否存在,如果不存在,再去数据库中查询 8 * 这就是hibernate的一级缓存(session缓存) 9 */ 10 List<Student> stus = (List<Student>)session.createQuery("from Student") 11 .setFirstResult(0).setMaxResults(30).list(); 12 Student stu = (Student)session.load(Student.class, 1); 13 System.out.println(stu.getName() + "-----------"); 14 } 15 catch (Exception e) 16 { 17 e.printStackTrace(); 18 } 19 finally 20 { 21 HibernateUtil.close(session); 22 } 23 /** 24 * 当session关闭以后,session的一级缓存也就没有了,这时就又会去数据库中查询 25 */ 26 session = HibernateUtil.openSession(); 27 Student stu = (Student)session.load(Student.class, 1); 28 System.out.println(stu.getName() + "-----------");
Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.sex as sex2_, student0_.rid as rid2_ from t_student student0_ limit ? Hibernate: select student0_.id as id2_2_, student0_.name as name2_2_, student0_.sex as sex2_2_, student0_.rid as rid2_2_, classroom1_.id as id1_0_, classroom1_.name as name1_0_, classroom1_.sid as sid1_0_, special2_.id as id0_1_, special2_.name as name0_1_, special2_.type as type0_1_ from t_student student0_ left outer join t_classroom classroom1_ on student0_.rid=classroom1_.id left outer join t_special special2_ on classroom1_.sid=special2_.id where student0_.id=?
我们看到此时会发出两条sql语句,因为session关闭以后,一级缓存就不存在了,所以如果再查询的时候,就会再发sql。要解决这种问题,我们应该怎么做呢?这就要我们来配置hibernate的二级缓存了,也就是sessionFactory级别的缓存。
三、二级缓存(sessionFactory级别)
使用hibernate二级缓存,我们首先需要对其进行配置,配置步骤如下:
1.hibernate并没有提供相应的二级缓存的组件,所以需要加入额外的二级缓存包,常用的二级缓存包是EHcache。这个我们在下载好的hibernate的lib->optional->ehcache下可以找到(我这里使用的hibernate4.1.7版本),然后将里面的几个jar包导入即可。
2.在hibernate.cfg.xml配置文件中配置我们二级缓存的一些属性:
1 <!-- 开启二级缓存 --> 2 <property name="hibernate.cache.use_second_level_cache">true</property> 3 <!-- 二级缓存的提供类 在hibernate4.0版本以后我们都是配置这个属性来指定二级缓存的提供类--> 4 <property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property> 5 <!-- 二级缓存配置文件的位置 --> 6 <property name="hibernate.cache.provider_configuration_file_resource_path">ehcache.xml</property>
我这里使用的是hibernate4.1.7版本,如果是使用hibernate3的版本的话,那么二级缓存的提供类则要配置成这个:
1 <!--这个类在4.0版本以后已经不建议被使用了--> 2 <property name="hibernate.cache.provider_class">net.sf.ehcache.hibernate.EhCacheProvider</property>
3.配置hibernate的二级缓存是通过使用 ehcache的缓存包,所以我们需要创建一个 ehcache.xml 的配置文件,来配置我们的缓存信息,将其放到项目根目录下
1 <ehcache> 2 3 <!-- Sets the path to the directory where cache .data files are created. 4 5 If the path is a Java System Property it is replaced by 6 its value in the running VM. 7 8 The following properties are translated: 9 user.home - User's home directory 10 user.dir - User's current working directory 11 java.io.tmpdir - Default temp file path --> 12 13 <!--指定二级缓存存放在磁盘上的位置--> 14 <diskStore path="user.dir"/> 15 16 <!--我们可以给每个实体类指定一个对应的缓存,如果没有匹配到该类,则使用这个默认的缓存配置--> 17 <defaultCache 18 maxElementsInMemory="10000" //在内存中存放的最大对象数 19 eternal="false" //是否永久保存缓存,设置成false 20 timeToIdleSeconds="120" 21 timeToLiveSeconds="120" 22 overflowToDisk="true" //如果对象数量超过内存中最大的数,是否将其保存到磁盘中,设置成true 23 /> 24 25 <!-- 26 1、timeToLiveSeconds的定义是:以创建时间为基准开始计算的超时时长; 27 2、timeToIdleSeconds的定义是:在创建时间和最近访问时间中取出离现在最近的时间作为基准计算的超时时长; 28 3、如果仅设置了timeToLiveSeconds,则该对象的超时时间=创建时间+timeToLiveSeconds,假设为A; 29 4、如果没设置timeToLiveSeconds,则该对象的超时时间=max(创建时间,最近访问时间)+timeToIdleSeconds,假设为B; 30 5、如果两者都设置了,则取出A、B最少的值,即min(A,B),表示只要有一个超时成立即算超时。 31 32 --> 33 34 <!--可以给每个实体类指定一个配置文件,通过name属性指定,要使用类的全名--> 35 <cache name="com.xiaoluo.bean.Student" 36 maxElementsInMemory="10000" 37 eternal="false" 38 timeToIdleSeconds="300" 39 timeToLiveSeconds="600" 40 overflowToDisk="true" 41 /> 42 43 <cache name="sampleCache2" 44 maxElementsInMemory="1000" 45 eternal="true" 46 timeToIdleSeconds="0" 47 timeToLiveSeconds="0" 48 overflowToDisk="false" 49 /> --> 50 51 52 </ehcache>
①如果使用xml配置,我们需要在 Student.hbm.xml 中加上一下配置:
1 <hibernate-mapping package="com.xiaoluo.bean"> 2 <class name="Student" table="t_student"> 3 <!-- 二级缓存一般设置为只读的 --> 4 <cache usage="read-only"/> 5 <id name="id" type="int" column="id"> 6 <generator class="native"/> 7 </id> 8 <property name="name" column="name" type="string"></property> 9 <property name="sex" column="sex" type="string"></property> 10 <many-to-one name="room" column="rid" fetch="join"></many-to-one> 11 </class> 12 </hibernate-mapping>
二级缓存的使用策略一般有这几种:read-only、nonstrict-read-write、read-write、transactional。注意:我们通常使用二级缓存都是将其配置成 read-only ,即我们应当在那些不需要进行修改的实体类上使用二级缓存,否则如果对缓存进行读写的话,性能会变差,这样设置缓存就失去了意义。
②如果使用annotation配置,我们需要在Student这个类上加上这样一个注解:
1 @Entity 2 @Table(name="t_student") 3 @Cache(usage=CacheConcurrencyStrategy.READ_ONLY) // 表示开启二级缓存,并使用read-only策略 4 public class Student 5 { 6 private int id; 7 private String name; 8 private String sex; 9 private Classroom room; 10 ....... 11 }
这样我们的二级缓存配置就算完成了,接下来我们来通过测试用例测试下我们的二级缓存是否起作用
①二级缓存是sessionFactory级别的缓存
TestCase1:
1 public class TestSecondCache 2 { 3 @Test 4 public void testCache1() 5 { 6 Session session = null; 7 try 8 { 9 session = HibernateUtil.openSession(); 10 11 Student stu = (Student) session.load(Student.class, 1); 12 System.out.println(stu.getName() + "-----------"); 13 } 14 catch (Exception e) 15 { 16 e.printStackTrace(); 17 } 18 finally 19 { 20 HibernateUtil.close(session); 21 } 22 try 23 { 24 /** 25 * 即使当session关闭以后,因为配置了二级缓存,而二级缓存是sessionFactory级别的,所以会从缓存中取出该数据 26 * 只会发出一条sql语句 27 */ 28 session = HibernateUtil.openSession(); 29 Student stu = (Student) session.load(Student.class, 1); 30 System.out.println(stu.getName() + "-----------"); 31 /** 32 * 因为设置了二级缓存为read-only,所以不能对其进行修改 33 */ 34 session.beginTransaction(); 35 stu.setName("aaa"); 36 session.getTransaction().commit(); 37 } 38 catch (Exception e) 39 { 40 e.printStackTrace(); 41 session.getTransaction().rollback(); 42 } 43 finally 44 { 45 HibernateUtil.close(session); 46 } 47 }
Hibernate: select student0_.id as id2_2_, student0_.name as name2_2_, student0_.sex as sex2_2_, student0_.rid as rid2_2_, classroom1_.id as id1_0_, classroom1_.name as name1_0_, classroom1_.sid as sid1_0_, special2_.id as id0_1_, special2_.name as name0_1_, special2_.type as type0_1_ from t_student student0_ left outer join t_classroom classroom1_ on student0_.rid=classroom1_.id left outer join t_special special2_ on classroom1_.sid=special2_.id where student0_.id=? aaa----------- aaa-----------
因为二级缓存是sessionFactory级别的缓存,我们看到,在配置了二级缓存以后,当我们session关闭以后,我们再去查询对象的时候,此时hibernate首先会去二级缓存中查询是否有该对象,有就不会再发sql了。
②二级缓存缓存的仅仅是对象,如果查询出来的是对象的一些属性,则不会被加到缓存中去
TestCase2:
1 @Test 2 public void testCache2() 3 { 4 Session session = null; 5 try 6 { 7 session = HibernateUtil.openSession(); 8 9 /** 10 * 注意:二级缓存中缓存的仅仅是对象,而下面这里只保存了姓名和性别两个字段,所以 不会被加载到二级缓存里面 11 */ 12 List<Object[]> ls = (List<Object[]>) session 13 .createQuery("select stu.name, stu.sex from Student stu") 14 .setFirstResult(0).setMaxResults(30).list(); 15 } 16 catch (Exception e) 17 { 18 e.printStackTrace(); 19 } 20 finally 21 { 22 HibernateUtil.close(session); 23 } 24 try 25 { 26 /** 27 * 由于二级缓存缓存的是对象,所以此时会发出两条sql 28 */ 29 session = HibernateUtil.openSession(); 30 Student stu = (Student) session.load(Student.class, 1); 31 System.out.println(stu); 32 } 33 catch (Exception e) 34 { 35 e.printStackTrace(); 36 } 37 }
Hibernate: select student0_.name as col_0_0_, student0_.sex as col_1_0_ from t_student student0_ limit ? Hibernate: select student0_.id as id2_2_, student0_.name as name2_2_, student0_.sex as sex2_2_, student0_.rid as rid2_2_, classroom1_.id as id1_0_, classroom1_.name as name1_0_, classroom1_.sid as sid1_0_, special2_.id as id0_1_, special2_.name as name0_1_, special2_.type as type0_1_ from t_student student0_ left outer join t_classroom classroom1_ on student0_.rid=classroom1_.id left outer join t_special special2_ on classroom1_.sid=special2_.id where student0_.id=?
我们看到这个测试用例,如果我们只是取出对象的一些属性的话,则不会将其保存到二级缓存中去,因为二级缓存缓存的仅仅是对象。
③通过二级缓存来解决 N+1 的问题
TestCase3:
1 @Test 2 public void testCache3() 3 { 4 Session session = null; 5 try 6 { 7 session = HibernateUtil.openSession(); 8 /** 9 * 将查询出来的Student对象缓存到二级缓存中去 10 */ 11 List<Student> stus = (List<Student>) session.createQuery( 12 "select stu from Student stu").list(); 13 } 14 catch (Exception e) 15 { 16 e.printStackTrace(); 17 } 18 finally 19 { 20 HibernateUtil.close(session); 21 } 22 try 23 { 24 /** 25 * 由于学生的对象已经缓存在二级缓存中了,此时再使用iterate来获取对象的时候,首先会通过一条 26 * 取id的语句,然后在获取对象时去二级缓存中,如果发现就不会再发SQL,这样也就解决了N+1问题 27 * 而且内存占用也不多 28 */ 29 session = HibernateUtil.openSession(); 30 Iterator<Student> iterator = session.createQuery("from Student") 31 .iterate(); 32 for (; iterator.hasNext();) 33 { 34 Student stu = (Student) iterator.next(); 35 System.out.println(stu.getName()); 36 } 37 } 38 catch (Exception e) 39 { 40 e.printStackTrace(); 41 } 42 }
当我们如果需要查询出两次对象的时候,可以使用二级缓存来解决N+1的问题。
④二级缓存会缓存 hql 语句吗?
TestCase4:
1 @Test 2 public void testCache4() 3 { 4 Session session = null; 5 try 6 { 7 session = HibernateUtil.openSession(); 8 List<Student> ls = session.createQuery("from Student") 9 .setFirstResult(0).setMaxResults(50).list(); 10 } 11 catch (Exception e) 12 { 13 e.printStackTrace(); 14 } 15 finally 16 { 17 HibernateUtil.close(session); 18 } 19 try 20 { 21 /** 22 * 使用List会发出两条一模一样的sql,此时如果希望不发sql就需要使用查询缓存 23 */ 24 session = HibernateUtil.openSession(); 25 List<Student> ls = session.createQuery("from Student") 26 .setFirstResult(0).setMaxResults(50).list(); 27 Iterator<Student> stu = ls.iterator(); 28 for(;stu.hasNext();) 29 { 30 Student student = stu.next(); 31 System.out.println(student.getName()); 32 } 33 } 34 catch (Exception e) 35 { 36 e.printStackTrace(); 37 } 38 finally 39 { 40 HibernateUtil.close(session); 41 } 42 }
Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.sex as sex2_, student0_.rid as rid2_ from t_student student0_ limit ? Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.sex as sex2_, student0_.rid as rid2_ from t_student student0_ limit ?
我们看到,当我们如果通过 list() 去查询两次对象时,二级缓存虽然会缓存查询出来的对象,但是我们看到发出了两条相同的查询语句,这是因为二级缓存不会缓存我们的hql查询语句,要想解决这个问题,我们就要配置我们的查询缓存了。
四、查询缓存(sessionFactory级别)
我们如果要配置查询缓存,只需要在hibernate.cfg.xml中加入一条配置即可:
1 <!-- 开启查询缓存 --> 2 <property name="hibernate.cache.use_query_cache">true</property>
然后我们如果在查询hql语句时要使用查询缓存,就需要在查询语句后面设置这样一个方法:
1 List<Student> ls = session.createQuery("from Student where name like ?") 2 .setCacheable(true) //开启查询缓存,查询缓存也是SessionFactory级别的缓存 3 .setParameter(0, "%王%") 4 .setFirstResult(0).setMaxResults(50).list();
如果是在annotation中,我们还需要在这个类上加上这样一个注解:@Cacheable
接下来我们来通过测试用例来看看我们的查询缓存
①查询缓存也是sessionFactory级别的缓存
TestCase1:
1 @Test 2 public void test2() { 3 Session session = null; 4 try { 5 /** 6 * 此时会发出一条sql取出所有的学生信息 7 */ 8 session = HibernateUtil.openSession(); 9 List<Student> ls = session.createQuery("from Student") 10 .setCacheable(true) //开启查询缓存,查询缓存也是sessionFactory级别的缓存 11 .setFirstResult(0).setMaxResults(50).list(); 12 Iterator<Student> stus = ls.iterator(); 13 for(;stus.hasNext();) { 14 Student stu = stus.next(); 15 System.out.println(stu.getName()); 16 } 17 } catch (Exception e) { 18 e.printStackTrace(); 19 } finally { 20 HibernateUtil.close(session); 21 } 22 try { 23 /** 24 * 此时会发出一条sql取出所有的学生信息 25 */ 26 session = HibernateUtil.openSession(); 27 List<Student> ls = session.createQuery("from Student") 28 .setCacheable(true) //开启查询缓存,查询缓存也是sessionFactory级别的缓存 29 .setFirstResult(0).setMaxResults(50).list(); 30 Iterator<Student> stus = ls.iterator(); 31 for(;stus.hasNext();) { 32 Student stu = stus.next(); 33 System.out.println(stu.getName()); 34 } 35 } catch (Exception e) { 36 e.printStackTrace(); 37 } finally { 38 HibernateUtil.close(session); 39 } 40 }
Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.sex as sex2_, student0_.rid as rid2_ from t_student student0_ limit ?
我们看到,此时如果我们发出两条相同的语句,hibernate也只会发出一条sql,因为已经开启了查询缓存了,并且查询缓存也是sessionFactory级别的
②只有当 HQL 查询语句完全相同时,连参数设置都要相同,此时查询缓存才有效
TestCase2:
1 @Test 2 public void test3() { 3 Session session = null; 4 try { 5 /** 6 * 此时会发出一条sql取出所有的学生信息 7 */ 8 session = HibernateUtil.openSession(); 9 List<Student> ls = session.createQuery("from Student where name like ?") 10 .setCacheable(true)//开启查询缓存,查询缓存也是SessionFactory级别的缓存 11 .setParameter(0, "%王%") 12 .setFirstResult(0).setMaxResults(50).list(); 13 Iterator<Student> stus = ls.iterator(); 14 for(;stus.hasNext();) { 15 Student stu = stus.next(); 16 System.out.println(stu.getName()); 17 } 18 } catch (Exception e) { 19 e.printStackTrace(); 20 } finally { 21 HibernateUtil.close(session); 22 } 23 session = null; 24 try { 25 /** 26 * 此时会发出一条sql取出所有的学生信息 27 */ 28 session = HibernateUtil.openSession(); 29 /** 30 * 只有当HQL完全相同的时候,连参数都要相同,查询缓存才有效 31 */ 32 // List<Student> ls = session.createQuery("from Student where name like ?") 33 // .setCacheable(true)//开启查询缓存,查询缓存也是SessionFactory级别的缓存 34 // .setParameter(0, "%王%") 35 // .setFirstResult(0).setMaxResults(50).list(); 36 List<Student> ls = session.createQuery("from Student where name like ?") 37 .setCacheable(true)//开启查询缓存,查询缓存也是SessionFactory级别的缓存 38 .setParameter(0, "%张%") 39 .setFirstResult(0).setMaxResults(50).list(); 40 Iterator<Student> stus = ls.iterator(); 41 for(;stus.hasNext();) { 42 Student stu = stus.next(); 43 System.out.println(stu.getName()); 44 } 45 } catch (Exception e) { 46 e.printStackTrace(); 47 } finally { 48 HibernateUtil.close(session); 49 } 50 }
Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.sex as sex2_, student0_.rid as rid2_ from t_student student0_ where student0_.name like ? limit ? Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.sex as sex2_, student0_.rid as rid2_ from t_student student0_ where student0_.name like ? limit ?
我们看到,如果我们的hql查询语句不同的话,我们的查询缓存也没有作用
③查询缓存也能引起 N+1 的问题
查询缓存也能引起 N+1 的问题,我们这里首先先将 Student 对象上的二级缓存先注释掉:
1 <!-- 二级缓存一般设置为只读的 --> 2 <!-- <cache usage="read-only"/> -->
TestCase4:
1 @Test 2 public void test4() { 3 Session session = null; 4 try { 5 /** 6 * 查询缓存缓存的不是对象而是id 7 */ 8 session = HibernateUtil.openSession(); 9 List<Student> ls = session.createQuery("from Student where name like ?") 10 .setCacheable(true)//开启查询缓存,查询缓存也是SessionFactory级别的缓存 11 .setParameter(0, "%王%") 12 .setFirstResult(0).setMaxResults(50).list(); 13 Iterator<Student> stus = ls.iterator(); 14 for(;stus.hasNext();) { 15 Student stu = stus.next(); 16 System.out.println(stu.getName()); 17 } 18 } catch (Exception e) { 19 e.printStackTrace(); 20 } finally { 21 HibernateUtil.close(session); 22 } 23 24 session = null; 25 try { 26 /** 27 * 查询缓存缓存的是id,此时由于在缓存中已经存在了这样的一组学生数据,但是仅仅只是缓存了 28 * id,所以此处会发出大量的sql语句根据id取对象,这也是发现N+1问题的第二个原因 29 * 所以如果使用查询缓存必须开启二级缓存 30 */ 31 session = HibernateUtil.openSession(); 32 List<Student> ls = session.createQuery("from Student where name like ?") 33 .setCacheable(true)//开启查询缓存,查询缓存也是SessionFactory级别的缓存 34 .setParameter(0, "%王%") 35 .setFirstResult(0).setMaxResults(50).list(); 36 Iterator<Student> stus = ls.iterator(); 37 for(;stus.hasNext();) { 38 Student stu = stus.next(); 39 System.out.println(stu.getName()); 40 } 41 } catch (Exception e) { 42 e.printStackTrace(); 43 } finally { 44 HibernateUtil.close(session); 45 } 46 }
Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.sex as sex2_, student0_.rid as rid2_ from t_student student0_ where student0_.name like ? limit ? Hibernate: select student0_.id as id2_2_, student0_.name as name2_2_, student0_.sex as sex2_2_, student0_.rid as rid2_2_, classroom1_.id as id1_0_, classroom1_.name as name1_0_, classroom1_.sid as sid1_0_, special2_.id as id0_1_, special2_.name as name0_1_, special2_.type as type0_1_ from t_student student0_ left outer join t_classroom classroom1_ on student0_.rid=classroom1_.id left outer join t_special special2_ on classroom1_.sid=special2_.id where student0_.id=? Hibernate: select student0_.id as id2_2_, student0_.name as name2_2_, student0_.sex as sex2_2_, student0_.rid as rid2_2_, classroom1_.id as id1_0_, classroom1_.name as name1_0_, classroom1_.sid as sid1_0_, special2_.id as id0_1_, special2_.name as name0_1_, special2_.type as type0_1_ from t_student student0_ left outer join t_classroom classroom1_ on student0_.rid=classroom1_.id left outer join t_special special2_ on classroom1_.sid=special2_.id where student0_.id=? Hibernate: select student0_.id as id2_2_, student0_.name as name2_2_, student0_.sex as sex2_2_, student0_.rid as rid2_2_, classroom1_.id as id1_0_, classroom1_.name as name1_0_, classroom1_.sid as sid1_0_, special2_.id as id0_1_, special2_.name as name0_1_, special2_.type as type0_1_ from t_student student0_ left outer join t_classroom classroom1_ on student0_.rid=classroom1_.id left outer join t_special special2_ on classroom1_.sid=special2_.id where student0_.id=? Hibernate: select student0_.id as id2_2_, student0_.name as name2_2_, student0_.sex as sex2_2_, student0_.rid as rid2_2_, classroom1_.id as id1_0_, classroom1_.name as name1_0_, classroom1_.sid as sid1_0_, special2_.id as id0_1_, special2_.name as name0_1_, special2_.type as type0_1_ from t_student student0_ left outer join t_classroom classroom1_ on student0_.rid=classroom1_.id left outer join t_special special2_ on classroom1_.sid=special2_.id where student0_.id=? .........................
我们看到,当我们将二级缓存注释掉以后,在使用查询缓存时,也会出现 N+1 的问题,为什么呢?
因为查询缓存缓存的也仅仅是对象的id,所以第一条 sql 也是将对象的id都查询出来,但是当我们后面如果要得到每个对象的信息的时候,此时又会发sql语句去查询,所以,如果要使用查询缓存,我们一定也要开启我们的二级缓存,这样就不会出现 N+1 问题了
在本篇随笔里将会分析一下hibernate的缓存机制,包括一级缓存(session级别)、二级缓存(sessionFactory级别)以及查询缓存,当然还要讨论下我们的N+1的问题。
随笔虽长,但我相信看完的朋友绝对能对hibernate的 N+1问题以及缓存有更深的了解。
一、N+1问题
首先我们来探讨一下N+1的问题,我们先通过一个例子来看一下,什么是N+1问题:
list()获得对象:
1 /** 2 * 此时会发出一条sql,将30个学生全部查询出来 3 */ 4 List<Student> ls = (List<Student>)session.createQuery("from Student") 5 .setFirstResult(0).setMaxResults(30).list(); 6 Iterator<Student> stus = ls.iterator(); 7 for(;stus.hasNext();) 8 { 9 Student stu = (Student)stus.next(); 10 System.out.println(stu.getName()); 11 }
如果通过list()方法来获得对象,毫无疑问,hibernate会发出一条sql语句,将所有的对象查询出来,这点相信大家都能理解
Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.rid as rid2_, student0_.sex as sex2_ from t_student student0_ limit ?
那么,我们再来看看iterator()这种情况
iterator()获得对象
1 /** 2 * 如果使用iterator方法返回列表,对于hibernate而言,它仅仅只是发出取id列表的sql 3 * 在查询相应的具体的某个学生信息时,会发出相应的SQL去取学生信息 4 * 这就是典型的N+1问题 5 * 存在iterator的原因是,有可能会在一个session中查询两次数据,如果使用list每一次都会把所有的对象查询上来 6 * 而是要iterator仅仅只会查询id,此时所有的对象已经存储在一级缓存(session的缓存)中,可以直接获取 7 */ 8 Iterator<Student> stus = (Iterator<Student>)session.createQuery("from Student") 9 .setFirstResult(0).setMaxResults(30).iterate(); 10 for(;stus.hasNext();) 11 { 12 Student stu = (Student)stus.next(); 13 System.out.println(stu.getName()); 14 }
在执行完上述的测试用例后,我们来看看控制台的输出,看会发出多少条 sql 语句:
Hibernate: select student0_.id as col_0_0_ from t_student student0_ limit ? Hibernate: select student0_.id as id2_0_, student0_.name as name2_0_, student0_.rid as rid2_0_, student0_.sex as sex2_0_ from t_student student0_ where student0_.id=? 沈凡 Hibernate: select student0_.id as id2_0_, student0_.name as name2_0_, student0_.rid as rid2_0_, student0_.sex as sex2_0_ from t_student student0_ where student0_.id=? 王志名 Hibernate: select student0_.id as id2_0_, student0_.name as name2_0_, student0_.rid as rid2_0_, student0_.sex as sex2_0_ from t_student student0_ where student0_.id=? 叶敦
.........
我们看到,当如果通过iterator()方法来获得我们对象的时候,hibernate首先会发出1条sql去查询出所有对象的 id 值,当我们如果需要查询到某个对象的具体信息的时候,hibernate此时会根据查询出来的 id 值再发sql语句去从数据库中查询对象的信息,这就是典型的 N+1 的问题。
那么这种 N+1 问题我们如何解决呢,其实我们只需要使用 list() 方法来获得对象即可。但是既然可以通过 list() 我们就不会出现 N+1的问题,那么我们为什么还要保留 iterator()这种形式呢?我们考虑这样一种情况,如果我们需要在一个session当中要两次查询出很多对象,此时我们如果写两条 list()时,hibernate此时会发出两条 sql 语句,而且这两条语句是一样的,但是我们如果第一条语句使用 list(),而第二条语句使用 iterator()的话,此时我们也会发两条sql语句,但是第二条语句只会将查询出对象的id,所以相对应取出所有的对象而已,显然这样可以节省内存,而如果再要获取对象的时候,因为第一条语句已经将对象都查询出来了,此时会将对象保存到session的一级缓存中去,所以再次查询时,就会首先去缓存中查找,如果找到,则不发sql语句了。这里就牵涉到了接下来这个概念:hibernate的一级缓存。
二、一级缓存(session级别)
我们来看看hibernate提供的一级缓存:
1 /** 2 * 此时会发出一条sql,将所有学生全部查询出来,并放到session的一级缓存当中 3 * 当再次查询学生信息时,会首先去缓存中看是否存在,如果不存在,再去数据库中查询 4 * 这就是hibernate的一级缓存(session缓存) 5 */ 6 List<Student> stus = (List<Student>)session.createQuery("from Student") 7 .setFirstResult(0).setMaxResults(30).list(); 8 Student stu = (Student)session.load(Student.class, 1);
我们来看看控制台输出:
Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.rid as rid2_, student0_.sex as sex2_ from t_student student0_ limit ?
我们看到此时hibernate仅仅只会发出一条 sql 语句,因为第一行代码就会将整个的对象查询出来,放到session的一级缓存中去,当我如果需要再次查询学生对象时,此时首先会去缓存中看是否存在该对象,如果存在,则直接从缓存中取出,就不会再发sql了,但是要注意一点:hibernate的一级缓存是session级别的,所以如果session关闭后,缓存就没了,此时就会再次发sql去查数据库。
1 try 2 { 3 session = HibernateUtil.openSession(); 4 5 /** 6 * 此时会发出一条sql,将所有学生全部查询出来,并放到session的一级缓存当中 7 * 当再次查询学生信息时,会首先去缓存中看是否存在,如果不存在,再去数据库中查询 8 * 这就是hibernate的一级缓存(session缓存) 9 */ 10 List<Student> stus = (List<Student>)session.createQuery("from Student") 11 .setFirstResult(0).setMaxResults(30).list(); 12 Student stu = (Student)session.load(Student.class, 1); 13 System.out.println(stu.getName() + "-----------"); 14 } 15 catch (Exception e) 16 { 17 e.printStackTrace(); 18 } 19 finally 20 { 21 HibernateUtil.close(session); 22 } 23 /** 24 * 当session关闭以后,session的一级缓存也就没有了,这时就又会去数据库中查询 25 */ 26 session = HibernateUtil.openSession(); 27 Student stu = (Student)session.load(Student.class, 1); 28 System.out.println(stu.getName() + "-----------");
Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.sex as sex2_, student0_.rid as rid2_ from t_student student0_ limit ? Hibernate: select student0_.id as id2_2_, student0_.name as name2_2_, student0_.sex as sex2_2_, student0_.rid as rid2_2_, classroom1_.id as id1_0_, classroom1_.name as name1_0_, classroom1_.sid as sid1_0_, special2_.id as id0_1_, special2_.name as name0_1_, special2_.type as type0_1_ from t_student student0_ left outer join t_classroom classroom1_ on student0_.rid=classroom1_.id left outer join t_special special2_ on classroom1_.sid=special2_.id where student0_.id=?
我们看到此时会发出两条sql语句,因为session关闭以后,一级缓存就不存在了,所以如果再查询的时候,就会再发sql。要解决这种问题,我们应该怎么做呢?这就要我们来配置hibernate的二级缓存了,也就是sessionFactory级别的缓存。
三、二级缓存(sessionFactory级别)
使用hibernate二级缓存,我们首先需要对其进行配置,配置步骤如下:
1.hibernate并没有提供相应的二级缓存的组件,所以需要加入额外的二级缓存包,常用的二级缓存包是EHcache。这个我们在下载好的hibernate的lib->optional->ehcache下可以找到(我这里使用的hibernate4.1.7版本),然后将里面的几个jar包导入即可。
2.在hibernate.cfg.xml配置文件中配置我们二级缓存的一些属性:
1 <!-- 开启二级缓存 --> 2 <property name="hibernate.cache.use_second_level_cache">true</property> 3 <!-- 二级缓存的提供类 在hibernate4.0版本以后我们都是配置这个属性来指定二级缓存的提供类--> 4 <property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property> 5 <!-- 二级缓存配置文件的位置 --> 6 <property name="hibernate.cache.provider_configuration_file_resource_path">ehcache.xml</property>
我这里使用的是hibernate4.1.7版本,如果是使用hibernate3的版本的话,那么二级缓存的提供类则要配置成这个:
1 <!--这个类在4.0版本以后已经不建议被使用了--> 2 <property name="hibernate.cache.provider_class">net.sf.ehcache.hibernate.EhCacheProvider</property>
3.配置hibernate的二级缓存是通过使用 ehcache的缓存包,所以我们需要创建一个 ehcache.xml 的配置文件,来配置我们的缓存信息,将其放到项目根目录下
1 <ehcache> 2 3 <!-- Sets the path to the directory where cache .data files are created. 4 5 If the path is a Java System Property it is replaced by 6 its value in the running VM. 7 8 The following properties are translated: 9 user.home - User's home directory 10 user.dir - User's current working directory 11 java.io.tmpdir - Default temp file path --> 12 13 <!--指定二级缓存存放在磁盘上的位置--> 14 <diskStore path="user.dir"/> 15 16 <!--我们可以给每个实体类指定一个对应的缓存,如果没有匹配到该类,则使用这个默认的缓存配置--> 17 <defaultCache 18 maxElementsInMemory="10000" //在内存中存放的最大对象数 19 eternal="false" //是否永久保存缓存,设置成false 20 timeToIdleSeconds="120" 21 timeToLiveSeconds="120" 22 overflowToDisk="true" //如果对象数量超过内存中最大的数,是否将其保存到磁盘中,设置成true 23 /> 24 25 <!-- 26 1、timeToLiveSeconds的定义是:以创建时间为基准开始计算的超时时长; 27 2、timeToIdleSeconds的定义是:在创建时间和最近访问时间中取出离现在最近的时间作为基准计算的超时时长; 28 3、如果仅设置了timeToLiveSeconds,则该对象的超时时间=创建时间+timeToLiveSeconds,假设为A; 29 4、如果没设置timeToLiveSeconds,则该对象的超时时间=max(创建时间,最近访问时间)+timeToIdleSeconds,假设为B; 30 5、如果两者都设置了,则取出A、B最少的值,即min(A,B),表示只要有一个超时成立即算超时。 31 32 --> 33 34 <!--可以给每个实体类指定一个配置文件,通过name属性指定,要使用类的全名--> 35 <cache name="com.xiaoluo.bean.Student" 36 maxElementsInMemory="10000" 37 eternal="false" 38 timeToIdleSeconds="300" 39 timeToLiveSeconds="600" 40 overflowToDisk="true" 41 /> 42 43 <cache name="sampleCache2" 44 maxElementsInMemory="1000" 45 eternal="true" 46 timeToIdleSeconds="0" 47 timeToLiveSeconds="0" 48 overflowToDisk="false" 49 /> --> 50 51 52 </ehcache>
①如果使用xml配置,我们需要在 Student.hbm.xml 中加上一下配置:
1 <hibernate-mapping package="com.xiaoluo.bean"> 2 <class name="Student" table="t_student"> 3 <!-- 二级缓存一般设置为只读的 --> 4 <cache usage="read-only"/> 5 <id name="id" type="int" column="id"> 6 <generator class="native"/> 7 </id> 8 <property name="name" column="name" type="string"></property> 9 <property name="sex" column="sex" type="string"></property> 10 <many-to-one name="room" column="rid" fetch="join"></many-to-one> 11 </class> 12 </hibernate-mapping>
二级缓存的使用策略一般有这几种:read-only、nonstrict-read-write、read-write、transactional。注意:我们通常使用二级缓存都是将其配置成 read-only ,即我们应当在那些不需要进行修改的实体类上使用二级缓存,否则如果对缓存进行读写的话,性能会变差,这样设置缓存就失去了意义。
②如果使用annotation配置,我们需要在Student这个类上加上这样一个注解:
1 @Entity 2 @Table(name="t_student") 3 @Cache(usage=CacheConcurrencyStrategy.READ_ONLY) // 表示开启二级缓存,并使用read-only策略 4 public class Student 5 { 6 private int id; 7 private String name; 8 private String sex; 9 private Classroom room; 10 ....... 11 }
这样我们的二级缓存配置就算完成了,接下来我们来通过测试用例测试下我们的二级缓存是否起作用
①二级缓存是sessionFactory级别的缓存
TestCase1:
1 public class TestSecondCache 2 { 3 @Test 4 public void testCache1() 5 { 6 Session session = null; 7 try 8 { 9 session = HibernateUtil.openSession(); 10 11 Student stu = (Student) session.load(Student.class, 1); 12 System.out.println(stu.getName() + "-----------"); 13 } 14 catch (Exception e) 15 { 16 e.printStackTrace(); 17 } 18 finally 19 { 20 HibernateUtil.close(session); 21 } 22 try 23 { 24 /** 25 * 即使当session关闭以后,因为配置了二级缓存,而二级缓存是sessionFactory级别的,所以会从缓存中取出该数据 26 * 只会发出一条sql语句 27 */ 28 session = HibernateUtil.openSession(); 29 Student stu = (Student) session.load(Student.class, 1); 30 System.out.println(stu.getName() + "-----------"); 31 /** 32 * 因为设置了二级缓存为read-only,所以不能对其进行修改 33 */ 34 session.beginTransaction(); 35 stu.setName("aaa"); 36 session.getTransaction().commit(); 37 } 38 catch (Exception e) 39 { 40 e.printStackTrace(); 41 session.getTransaction().rollback(); 42 } 43 finally 44 { 45 HibernateUtil.close(session); 46 } 47 }
Hibernate: select student0_.id as id2_2_, student0_.name as name2_2_, student0_.sex as sex2_2_, student0_.rid as rid2_2_, classroom1_.id as id1_0_, classroom1_.name as name1_0_, classroom1_.sid as sid1_0_, special2_.id as id0_1_, special2_.name as name0_1_, special2_.type as type0_1_ from t_student student0_ left outer join t_classroom classroom1_ on student0_.rid=classroom1_.id left outer join t_special special2_ on classroom1_.sid=special2_.id where student0_.id=? aaa----------- aaa-----------
因为二级缓存是sessionFactory级别的缓存,我们看到,在配置了二级缓存以后,当我们session关闭以后,我们再去查询对象的时候,此时hibernate首先会去二级缓存中查询是否有该对象,有就不会再发sql了。
②二级缓存缓存的仅仅是对象,如果查询出来的是对象的一些属性,则不会被加到缓存中去
TestCase2:
1 @Test 2 public void testCache2() 3 { 4 Session session = null; 5 try 6 { 7 session = HibernateUtil.openSession(); 8 9 /** 10 * 注意:二级缓存中缓存的仅仅是对象,而下面这里只保存了姓名和性别两个字段,所以 不会被加载到二级缓存里面 11 */ 12 List<Object[]> ls = (List<Object[]>) session 13 .createQuery("select stu.name, stu.sex from Student stu") 14 .setFirstResult(0).setMaxResults(30).list(); 15 } 16 catch (Exception e) 17 { 18 e.printStackTrace(); 19 } 20 finally 21 { 22 HibernateUtil.close(session); 23 } 24 try 25 { 26 /** 27 * 由于二级缓存缓存的是对象,所以此时会发出两条sql 28 */ 29 session = HibernateUtil.openSession(); 30 Student stu = (Student) session.load(Student.class, 1); 31 System.out.println(stu); 32 } 33 catch (Exception e) 34 { 35 e.printStackTrace(); 36 } 37 }
Hibernate: select student0_.name as col_0_0_, student0_.sex as col_1_0_ from t_student student0_ limit ? Hibernate: select student0_.id as id2_2_, student0_.name as name2_2_, student0_.sex as sex2_2_, student0_.rid as rid2_2_, classroom1_.id as id1_0_, classroom1_.name as name1_0_, classroom1_.sid as sid1_0_, special2_.id as id0_1_, special2_.name as name0_1_, special2_.type as type0_1_ from t_student student0_ left outer join t_classroom classroom1_ on student0_.rid=classroom1_.id left outer join t_special special2_ on classroom1_.sid=special2_.id where student0_.id=?
我们看到这个测试用例,如果我们只是取出对象的一些属性的话,则不会将其保存到二级缓存中去,因为二级缓存缓存的仅仅是对象。
③通过二级缓存来解决 N+1 的问题
TestCase3:
1 @Test 2 public void testCache3() 3 { 4 Session session = null; 5 try 6 { 7 session = HibernateUtil.openSession(); 8 /** 9 * 将查询出来的Student对象缓存到二级缓存中去 10 */ 11 List<Student> stus = (List<Student>) session.createQuery( 12 "select stu from Student stu").list(); 13 } 14 catch (Exception e) 15 { 16 e.printStackTrace(); 17 } 18 finally 19 { 20 HibernateUtil.close(session); 21 } 22 try 23 { 24 /** 25 * 由于学生的对象已经缓存在二级缓存中了,此时再使用iterate来获取对象的时候,首先会通过一条 26 * 取id的语句,然后在获取对象时去二级缓存中,如果发现就不会再发SQL,这样也就解决了N+1问题 27 * 而且内存占用也不多 28 */ 29 session = HibernateUtil.openSession(); 30 Iterator<Student> iterator = session.createQuery("from Student") 31 .iterate(); 32 for (; iterator.hasNext();) 33 { 34 Student stu = (Student) iterator.next(); 35 System.out.println(stu.getName()); 36 } 37 } 38 catch (Exception e) 39 { 40 e.printStackTrace(); 41 } 42 }
当我们如果需要查询出两次对象的时候,可以使用二级缓存来解决N+1的问题。
④二级缓存会缓存 hql 语句吗?
TestCase4:
1 @Test 2 public void testCache4() 3 { 4 Session session = null; 5 try 6 { 7 session = HibernateUtil.openSession(); 8 List<Student> ls = session.createQuery("from Student") 9 .setFirstResult(0).setMaxResults(50).list(); 10 } 11 catch (Exception e) 12 { 13 e.printStackTrace(); 14 } 15 finally 16 { 17 HibernateUtil.close(session); 18 } 19 try 20 { 21 /** 22 * 使用List会发出两条一模一样的sql,此时如果希望不发sql就需要使用查询缓存 23 */ 24 session = HibernateUtil.openSession(); 25 List<Student> ls = session.createQuery("from Student") 26 .setFirstResult(0).setMaxResults(50).list(); 27 Iterator<Student> stu = ls.iterator(); 28 for(;stu.hasNext();) 29 { 30 Student student = stu.next(); 31 System.out.println(student.getName()); 32 } 33 } 34 catch (Exception e) 35 { 36 e.printStackTrace(); 37 } 38 finally 39 { 40 HibernateUtil.close(session); 41 } 42 }
Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.sex as sex2_, student0_.rid as rid2_ from t_student student0_ limit ? Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.sex as sex2_, student0_.rid as rid2_ from t_student student0_ limit ?
我们看到,当我们如果通过 list() 去查询两次对象时,二级缓存虽然会缓存查询出来的对象,但是我们看到发出了两条相同的查询语句,这是因为二级缓存不会缓存我们的hql查询语句,要想解决这个问题,我们就要配置我们的查询缓存了。
四、查询缓存(sessionFactory级别)
我们如果要配置查询缓存,只需要在hibernate.cfg.xml中加入一条配置即可:
1 <!-- 开启查询缓存 --> 2 <property name="hibernate.cache.use_query_cache">true</property>
然后我们如果在查询hql语句时要使用查询缓存,就需要在查询语句后面设置这样一个方法:
1 List<Student> ls = session.createQuery("from Student where name like ?") 2 .setCacheable(true) //开启查询缓存,查询缓存也是SessionFactory级别的缓存 3 .setParameter(0, "%王%") 4 .setFirstResult(0).setMaxResults(50).list();
如果是在annotation中,我们还需要在这个类上加上这样一个注解:@Cacheable
接下来我们来通过测试用例来看看我们的查询缓存
①查询缓存也是sessionFactory级别的缓存
TestCase1:
1 @Test 2 public void test2() { 3 Session session = null; 4 try { 5 /** 6 * 此时会发出一条sql取出所有的学生信息 7 */ 8 session = HibernateUtil.openSession(); 9 List<Student> ls = session.createQuery("from Student") 10 .setCacheable(true) //开启查询缓存,查询缓存也是sessionFactory级别的缓存 11 .setFirstResult(0).setMaxResults(50).list(); 12 Iterator<Student> stus = ls.iterator(); 13 for(;stus.hasNext();) { 14 Student stu = stus.next(); 15 System.out.println(stu.getName()); 16 } 17 } catch (Exception e) { 18 e.printStackTrace(); 19 } finally { 20 HibernateUtil.close(session); 21 } 22 try { 23 /** 24 * 此时会发出一条sql取出所有的学生信息 25 */ 26 session = HibernateUtil.openSession(); 27 List<Student> ls = session.createQuery("from Student") 28 .setCacheable(true) //开启查询缓存,查询缓存也是sessionFactory级别的缓存 29 .setFirstResult(0).setMaxResults(50).list(); 30 Iterator<Student> stus = ls.iterator(); 31 for(;stus.hasNext();) { 32 Student stu = stus.next(); 33 System.out.println(stu.getName()); 34 } 35 } catch (Exception e) { 36 e.printStackTrace(); 37 } finally { 38 HibernateUtil.close(session); 39 } 40 }
Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.sex as sex2_, student0_.rid as rid2_ from t_student student0_ limit ?
我们看到,此时如果我们发出两条相同的语句,hibernate也只会发出一条sql,因为已经开启了查询缓存了,并且查询缓存也是sessionFactory级别的
②只有当 HQL 查询语句完全相同时,连参数设置都要相同,此时查询缓存才有效
TestCase2:
1 @Test 2 public void test3() { 3 Session session = null; 4 try { 5 /** 6 * 此时会发出一条sql取出所有的学生信息 7 */ 8 session = HibernateUtil.openSession(); 9 List<Student> ls = session.createQuery("from Student where name like ?") 10 .setCacheable(true)//开启查询缓存,查询缓存也是SessionFactory级别的缓存 11 .setParameter(0, "%王%") 12 .setFirstResult(0).setMaxResults(50).list(); 13 Iterator<Student> stus = ls.iterator(); 14 for(;stus.hasNext();) { 15 Student stu = stus.next(); 16 System.out.println(stu.getName()); 17 } 18 } catch (Exception e) { 19 e.printStackTrace(); 20 } finally { 21 HibernateUtil.close(session); 22 } 23 session = null; 24 try { 25 /** 26 * 此时会发出一条sql取出所有的学生信息 27 */ 28 session = HibernateUtil.openSession(); 29 /** 30 * 只有当HQL完全相同的时候,连参数都要相同,查询缓存才有效 31 */ 32 // List<Student> ls = session.createQuery("from Student where name like ?") 33 // .setCacheable(true)//开启查询缓存,查询缓存也是SessionFactory级别的缓存 34 // .setParameter(0, "%王%") 35 // .setFirstResult(0).setMaxResults(50).list(); 36 List<Student> ls = session.createQuery("from Student where name like ?") 37 .setCacheable(true)//开启查询缓存,查询缓存也是SessionFactory级别的缓存 38 .setParameter(0, "%张%") 39 .setFirstResult(0).setMaxResults(50).list(); 40 Iterator<Student> stus = ls.iterator(); 41 for(;stus.hasNext();) { 42 Student stu = stus.next(); 43 System.out.println(stu.getName()); 44 } 45 } catch (Exception e) { 46 e.printStackTrace(); 47 } finally { 48 HibernateUtil.close(session); 49 } 50 }
Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.sex as sex2_, student0_.rid as rid2_ from t_student student0_ where student0_.name like ? limit ? Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.sex as sex2_, student0_.rid as rid2_ from t_student student0_ where student0_.name like ? limit ?
我们看到,如果我们的hql查询语句不同的话,我们的查询缓存也没有作用
③查询缓存也能引起 N+1 的问题
查询缓存也能引起 N+1 的问题,我们这里首先先将 Student 对象上的二级缓存先注释掉:
1 <!-- 二级缓存一般设置为只读的 --> 2 <!-- <cache usage="read-only"/> -->
TestCase4:
1 @Test 2 public void test4() { 3 Session session = null; 4 try { 5 /** 6 * 查询缓存缓存的不是对象而是id 7 */ 8 session = HibernateUtil.openSession(); 9 List<Student> ls = session.createQuery("from Student where name like ?") 10 .setCacheable(true)//开启查询缓存,查询缓存也是SessionFactory级别的缓存 11 .setParameter(0, "%王%") 12 .setFirstResult(0).setMaxResults(50).list(); 13 Iterator<Student> stus = ls.iterator(); 14 for(;stus.hasNext();) { 15 Student stu = stus.next(); 16 System.out.println(stu.getName()); 17 } 18 } catch (Exception e) { 19 e.printStackTrace(); 20 } finally { 21 HibernateUtil.close(session); 22 } 23 24 session = null; 25 try { 26 /** 27 * 查询缓存缓存的是id,此时由于在缓存中已经存在了这样的一组学生数据,但是仅仅只是缓存了 28 * id,所以此处会发出大量的sql语句根据id取对象,这也是发现N+1问题的第二个原因 29 * 所以如果使用查询缓存必须开启二级缓存 30 */ 31 session = HibernateUtil.openSession(); 32 List<Student> ls = session.createQuery("from Student where name like ?") 33 .setCacheable(true)//开启查询缓存,查询缓存也是SessionFactory级别的缓存 34 .setParameter(0, "%王%") 35 .setFirstResult(0).setMaxResults(50).list(); 36 Iterator<Student> stus = ls.iterator(); 37 for(;stus.hasNext();) { 38 Student stu = stus.next(); 39 System.out.println(stu.getName()); 40 } 41 } catch (Exception e) { 42 e.printStackTrace(); 43 } finally { 44 HibernateUtil.close(session); 45 } 46 }
Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.sex as sex2_, student0_.rid as rid2_ from t_student student0_ where student0_.name like ? limit ? Hibernate: select student0_.id as id2_2_, student0_.name as name2_2_, student0_.sex as sex2_2_, student0_.rid as rid2_2_, classroom1_.id as id1_0_, classroom1_.name as name1_0_, classroom1_.sid as sid1_0_, special2_.id as id0_1_, special2_.name as name0_1_, special2_.type as type0_1_ from t_student student0_ left outer join t_classroom classroom1_ on student0_.rid=classroom1_.id left outer join t_special special2_ on classroom1_.sid=special2_.id where student0_.id=? Hibernate: select student0_.id as id2_2_, student0_.name as name2_2_, student0_.sex as sex2_2_, student0_.rid as rid2_2_, classroom1_.id as id1_0_, classroom1_.name as name1_0_, classroom1_.sid as sid1_0_, special2_.id as id0_1_, special2_.name as name0_1_, special2_.type as type0_1_ from t_student student0_ left outer join t_classroom classroom1_ on student0_.rid=classroom1_.id left outer join t_special special2_ on classroom1_.sid=special2_.id where student0_.id=? Hibernate: select student0_.id as id2_2_, student0_.name as name2_2_, student0_.sex as sex2_2_, student0_.rid as rid2_2_, classroom1_.id as id1_0_, classroom1_.name as name1_0_, classroom1_.sid as sid1_0_, special2_.id as id0_1_, special2_.name as name0_1_, special2_.type as type0_1_ from t_student student0_ left outer join t_classroom classroom1_ on student0_.rid=classroom1_.id left outer join t_special special2_ on classroom1_.sid=special2_.id where student0_.id=? Hibernate: select student0_.id as id2_2_, student0_.name as name2_2_, student0_.sex as sex2_2_, student0_.rid as rid2_2_, classroom1_.id as id1_0_, classroom1_.name as name1_0_, classroom1_.sid as sid1_0_, special2_.id as id0_1_, special2_.name as name0_1_, special2_.type as type0_1_ from t_student student0_ left outer join t_classroom classroom1_ on student0_.rid=classroom1_.id left outer join t_special special2_ on classroom1_.sid=special2_.id where student0_.id=? .........................
我们看到,当我们将二级缓存注释掉以后,在使用查询缓存时,也会出现 N+1 的问题,为什么呢?
因为查询缓存缓存的也仅仅是对象的id,所以第一条 sql 也是将对象的id都查询出来,但是当我们后面如果要得到每个对象的信息的时候,此时又会发sql语句去查询,所以,如果要使用查询缓存,我们一定也要开启我们的二级缓存,这样就不会出现 N+1 问题了