hibernate缓存机制以及n+1次查询有关问题
本文借鉴至:http://www.cnblogs.com/xiaoluo501395377/p/3377604.html
一:N+1次查询问题。
首先,什么是N+1次查询,我的理解是在使用session.createQuery("HQL语句").iterator()查询时第一次查询会去查询数据库中所有符合条件的记录的id,然后根据id逐一查询出每条记录的现象。
下面来看看代码:
/** * N+1次查询问题 */ @Test public void testOnePlusMore() { Session session = null; try{ session = HibernateUtils.getSession(); session.beginTransaction(); Query query = session.createQuery("from User"); Iterator<User> userIterator = query.iterate(); User user; while(userIterator.hasNext()) { user = userIterator.next(); System.out.println(user); } }catch (Exception e) { session.getTransaction().rollback(); } }
测试结果:
Hibernate: select user0_.id as col_0_0_ from t_user user0_ Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? User{id=1, name='aaa', age=12} Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? User{id=2, name='历史', age=22} Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? User{id=3, name='333', age=33} Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? User{id=4, name='阿什顿', age=22} Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? User{id=5, name='奥斯达', age=22} Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? User{id=6, name='33', age=44} Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? User{id=7, name='333', age=434}
为了做好对比,来看看list()方法查询所有的记录产生的现象是怎样的呢?
/** * 通过Query对象获取所有的记录 */ @Test public void testList() { Session session = null; try{ session = HibernateUtils.getSession(); session.beginTransaction(); Query query = session.createQuery("from User"); List<User> userList = query.list(); for(User user:userList) { System.out.println(user); } }catch (Exception e) { session.getTransaction().rollback(); } }
测试结果:
Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.name as name3_0_ from t_user user0_ User{id=1, name='aaa', age=12} User{id=2, name='历史', age=22} User{id=3, name='333', age=33} User{id=4, name='阿什顿', age=22} User{id=5, name='奥斯达', age=22} User{id=6, name='33', age=44} User{id=7, name='333', age=434}
从两次测试结果可以看出,iterator方法产生了大量的sql语句,并且查询n条记录的时候会产生n+1次查询,而这多的一次是去查询所有记录的ID而产生的。
其实到这里,我也比较疑惑既然list()方法能够一次性将所有的数据加载出来,那还用iterator()方法来干什么呢?感觉完全是多此一举的行为。但是,看到此文关联文章上面描述了一种场景:就是在一个session中两次查询出很多对象的时候,如果单单使用list()方法会造成两次查询的语句一样,这样就有一点浪费了。这里如果我们第一次使用list()方法,第二次就使用iterator()方法的话,此时我们也会发两条sql语句,但是第二条语句只会将查询出对象的id,所以相对应取出所有的对象而已,显然这样可以节省内存,而如果再要获取对象的时候,因为第一条语句已经将对象都查询出来了,此时会将对象保存到session的一级缓存中去,所以再次查询时,就会首先去缓存中查找,如果找到,则不发sql语句了。嗯,感觉这种说法确实是实际存在的,虽然没有在实际中碰到过,这里先记下吧。
二:hibernate一级缓存
前面的描述中又提到一个新东西:hibernate一级缓存。那么这个一级缓存又是个什么样的情况呢?
先用测试代码来看看这个现象吧:
/** * 测试一级缓存 */ @Test public void testFirstCache() { Session session = null; try{ session = HibernateUtils.getSession(); session.beginTransaction(); //第一次查询 User user = (User)session.get(User.class,1); System.out.println(user); System.out.println("----------------------------"); //第二次查询 User user1 = (User)session.get(User.class,1); System.out.println(user1); session.getTransaction().commit(); session.close(); }catch (Exception e) { session.getTransaction().rollback(); session.close(); } }
这里在一个session中进行两次查询(查询的对象要相同,或者第二次查询的对象已经在第一次查询中出现),这种情况下hibernate给我们的查询方式为:
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? User{id=1, name='aaa', age=12} ---------------------------- User{id=1, name='aaa', age=12}
可以看到这里至进行了一个查询,第二次查询的时候根本没有发送sql语句出去,可以说这就是我们的hibernate一级缓存产生的现象:hiberante会把查询的结果缓存在session中,session没有关闭之前要是有这个对象的缓存,那我们可以直接从这个session中取出这个对象而不用发送sql语句再次到数据库中去查询。注意:一级缓存的前提是session级别,也就是session范围内的缓存,在session没有关闭之前缓存的是有效的,但是session关闭之后缓存在那个session中的内容也不会存在了。
三:hibernate二级缓存(sessionFactory级别)
那么如果这样一种场景:我需要在session关闭之后还是能够取到之前查询出来的记录,这个该怎么办呢?这个就需要一个叫做二级缓存的东西的帮助了。
那什么是二级缓存呢?二级缓存是将查询的记录缓存在sessionFactory中。
先来说说具体使用吧:
1.hibernate并没有提供相应的二级缓存的组件,所以需要加入额外的二级缓存包,常用的二级缓存包是EHcache。这个我们在下载好的hibernate的lib->optional->ehcache下可以找到(我这里使用的hibernate4.1.7版本),然后将里面的几个jar包导入即可。
2.在hibernate.cfg.xml配置文件中配置我们二级缓存的一些属性:
<!-- 开启二级缓存 --> <property name="hibernate.cache.use_second_level_cache">true</property> <!-- 二级缓存的提供类 在hibernate4.0版本以后我们都是配置这个属性来指定二级缓存的提供类--> <property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property> <!-- 二级缓存配置文件的位置 --> <property name="hibernate.cache.provider_configuration_file_resource_path">ehcache.xml</property>
我这里使用的是hibernate4.3.8版本,如果是使用hibernate3的版本的话,那么二级缓存的提供类则要配置成这个:
<!--这个类在4.0版本以后已经不建议被使用了--> <property name="hibernate.cache.provider_class">net.sf.ehcache.hibernate.EhCacheProvider</property>
3.配置hibernate的二级缓存是通过使用 ehcache的缓存包,所以我们需要创建一个 ehcache.xml 的配置文件,来配置我们的缓存信息,将其放到项目根目录下
<ehcache> <!--指定二级缓存存放在磁盘上的位置--> <diskStore path="user.dir"/> <!--我们可以给每个实体类指定一个对应的缓存,如果没有匹配到该类,则使用这个默认的缓存配置--> <defaultCache maxElementsInMemory="10000" //在内存中存放的最大对象数 eternal="false" //是否永久保存缓存,设置成false timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" //如果对象数量超过内存中最大的数,是否将其保存到磁盘中,设置成true /> <!-- 1、timeToLiveSeconds的定义是:以创建时间为基准开始计算的超时时长; 2、timeToIdleSeconds的定义是:在创建时间和最近访问时间中取出离现在最近的时间作为基准计算的超时时长; 3、如果仅设置了timeToLiveSeconds,则该对象的超时时间=创建时间+timeToLiveSeconds,假设为A; 4、如果没设置timeToLiveSeconds,则该对象的超时时间=max(创建时间,最近访问时间)+timeToIdleSeconds,假设为B; 5、如果两者都设置了,则取出A、B最少的值,即min(A,B),表示只要有一个超时成立即算超时。 --> <!--可以给每个实体类指定一个配置文件,通过name属性指定,要使用类的全名--> <cache name="cn.bdx.po.User" maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true" /> </ehcache>
4.开启我们的二级缓存
①如果使用xml配置,我们需要在 User.hbm.xml 中加上一下配置:
<hibernate-mapping package="cn.bdx.po"> <class name="User" table="t_user"> <!-- 二级缓存一般设置为只读的 --> <cache usage="read-only"/> <id name="id" type="int" column="id"> <generator class="native"/> </id> <property name="name" column="name" type="string"></property> <property name="age" column="age" type="int"></property> </class> </hibernate-mapping>
二级缓存的使用策略一般有这几种:read-only、nonstrict-read-write、read-write、transactional。注意:我们通常使用二级缓存都是将其配置成 read-only ,即我们应当在那些不需要进行修改的实体类上使用二级缓存,否则如果对缓存进行读写的话,性能会变差,这样设置缓存就失去了意义。
②如果使用annotation配置,我们需要在User这个类上加上这样一个注解:
package cn.bdx.po; import org.hibernate.annotations.*; import org.hibernate.annotations.Cache; import javax.persistence.*; import javax.persistence.Entity; import javax.persistence.Table; /** * Created by Administrator on 2016/5/23. */ @Entity @Table(name="t_user") @Cache(usage = CacheConcurrencyStrategy.READ_ONLY)//表示开启二级缓存,并设置为只读 public class User{ @Id @GeneratedValue(strategy = GenerationType.AUTO) private int id; private String name; private int age; public int getId() { return id; } public void setId(int id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "User{" + "id=" + id + ", name='" + name + '\'' + ", age=" + age + '}'; } }
到这里基本上所有的配置算是完毕了,接下来是测试的代码:
/** * 测试二级缓存 */ @Test public void testEhcache() { Session session = null; try { session = HibernateUtils.getSession(); User user = (User) session.get(User.class, 1); System.out.println(user.getName() + "-----------"); } catch (Exception e) { e.printStackTrace(); } finally { session.close(); } try { /** * 即使当session关闭以后,因为配置了二级缓存,而二级缓存是sessionFactory级别的,所以会从缓存中取出该数据 * 只会发出一条sql语句 */ session = HibernateUtils.getSession(); User user = (User) session.get(User.class, 1); System.out.println(user.getName() + "-----------"); /** * 因为设置了二级缓存为read-only,所以不能对其进行修改 *//* session.beginTransaction(); user.setName("aaa"); session.getTransaction().commit();*/ } catch (Exception e) { e.printStackTrace(); session.getTransaction().rollback(); } finally { session.close(); } }
测试结果:
Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? aaa----------- aaa-----------从结果可以看出即使session关闭,我们仍然不会发送sql语句查询数据库取获取记录,这就是二级缓存的存在,在配置了二级缓存之后,查询的记录也会缓存到sessionFactory中,这样无论session是否关闭我们只要sessionFactory存在我们还是可以从缓存中获取记录。注意一点:二级缓存是缓存的对象,对于获取单个或多个字段这样的内容是不予以缓存的。
<!-- 开启查询缓存 --> <property name="hibernate.cache.use_query_cache">true</property>然后需要缓存的对象上面添加@Cacheable注解,表示要使用查询缓存;最后就是在代码中添加query.setCaheable(true),如下:
List<User> userList = session.createQuery("from User where id=?") .setCacheable(true)//开启查询缓存 .setParameter(0,1) .list();
/** * 测试查询缓存 */ @Test public void testQueryCache() { Session session = null; try{ session = HibernateUtils.getSession(); session.beginTransaction(); List<User> userList = session.createQuery("from User") .setCacheable(true)//开启查询缓存 .list(); session.getTransaction().commit(); for(User user:userList) { System.out.println(user); } }catch (Exception e) { session.getTransaction().rollback(); }finally { session.close(); } try{ session = HibernateUtils.getSession(); session.beginTransaction(); List<User> userList = session.createQuery("from User") .setCacheable(true)//开启查询缓存 .list(); session.getTransaction().commit(); for(User user:userList) { System.out.println(user); } }catch (Exception e) { session.getTransaction().rollback(); }finally { session.close(); } }
Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.name as name3_0_ from t_user user0_ User{id=1, name='aaa', age=12} User{id=2, name='历史', age=22} User{id=3, name='333', age=33} User{id=4, name='阿什顿', age=22} User{id=5, name='奥斯达', age=22} User{id=6, name='33', age=44} User{id=7, name='333', age=434} Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? Hibernate: select user0_.id as id1_0_0_, user0_.age as age2_0_0_, user0_.name as name3_0_0_ from t_user user0_ where user0_.id=? User{id=1, name='aaa', age=12} User{id=2, name='历史', age=22} User{id=3, name='333', age=33} User{id=4, name='阿什顿', age=22} User{id=5, name='奥斯达', age=22} User{id=6, name='33', age=44} User{id=7, name='333', age=434}
因为查询缓存缓存的也仅仅是对象的id,所以第一条 sql 也是将对象的id都查询出来,但是当我们后面如果要得到每个对象的信息的时候,此时又会发sql语句去查询,所以,如果要使用查询缓存,我们一定也要开启我们的二级缓存,这样就不会出现 N+1 问题了。
将前面关闭的二级缓存重新打开:
@Entity @Table(name="t_user") @Cache(usage = CacheConcurrencyStrategy.READ_ONLY)//表示开启二级缓存,并设置为只读 @Cacheable public class User{
这里重新打开二级缓存,然后运行上面的测试代码,
Hibernate: select user0_.id as id1_0_, user0_.age as age2_0_, user0_.name as name3_0_ from t_user user0_ User{id=1, name='aaa', age=12} User{id=2, name='历史', age=22} User{id=3, name='333', age=33} User{id=4, name='阿什顿', age=22} User{id=5, name='奥斯达', age=22} User{id=6, name='33', age=44} User{id=7, name='333', age=434} User{id=1, name='aaa', age=12} User{id=2, name='历史', age=22} User{id=3, name='333', age=33} User{id=4, name='阿什顿', age=22} User{id=5, name='奥斯达', age=22} User{id=6, name='33', age=44} User{id=7, name='333', age=434}
可以看到这次只发送了一次sql语句,并且也没有存在n+1次查询问题了。所以:在使用查询缓存的时候建议将二级缓存打开,这样可以避免n+1次查询问题。
使用查询缓存需要记住两点:1.查询缓存也是sessionFactory级别的缓存
2.②只有当 HQL 查询语句完全相同时,连参数设置都要相同,此时查询缓存才有效