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 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 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配置文件中配置我们二级缓存的一些属性:


        true
        
        org.hibernate.cache.ehcache.EhCacheRegionFactory
        
        ehcache.xml

 

我这里使用的是hibernate4.3.8版本,如果是使用hibernate3的版本的话,那么二级缓存的提供类则要配置成这个:

 


net.sf.ehcache.hibernate.EhCacheProvider

 

 3.配置hibernate的二级缓存是通过使用 ehcache的缓存包,所以我们需要创建一个 ehcache.xml 的配置文件,来配置我们的缓存信息,将其放到项目根目录下

 

 


    
    
    
    
    
    
    

4.开启我们的二级缓存

①如果使用xml配置,我们需要在 User.hbm.xml 中加上一下配置:


    
        
        
        
            
        
        
        
            

 二级缓存的使用策略一般有这几种: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存在我们还是可以从缓存中获取记录。 注意一点:二级缓存是缓存的对象,对于获取单个或多个字段这样的内容是不予以缓存的。
四:查询缓存(sessionFactory级别)
首先二级缓存不能缓存hql语句,所以这里用到查询缓存的作用就是缓存hql语句。
使用查询缓存,只需要在hibernate.cfg.xml中加入一条配置即可:
 
        true
 然后需要缓存的对象上面添加@Cacheable注解,表示要使用查询缓存;最后就是在代码中添加query.setCaheable(true),如下:
List 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 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 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}
说明一下:这里我关闭了二级缓存进行的测试,从测试结果可以看到,在两次都使用list()进行查询的时候,第二次会出现n+1次查询问题,为什么呢?

因为查询缓存缓存的也仅仅是对象的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 查询语句完全相同时,连参数设置都要相同,此时查询缓存才有效

你可能感兴趣的:(hibernate)