1. 程式人生 > >Hibernate 一級快取 二級快取 1+N問題 查詢快取

Hibernate 一級快取 二級快取 1+N問題 查詢快取

在本篇隨筆裡將會分析一下hibernate的快取機制,包括一級快取(session級別)、二級快取(sessionFactory級別)以及查詢快取,當然還要討論下我們的N+1的問題。

隨筆雖長,但我相信看完的朋友絕對能對hibernate的 N+1問題以及快取有更深的瞭解。

一、N+1問題

首先我們來探討一下N+1的問題,我們先通過一個例子來看一下,什麼是N+1問題:

list()獲得物件:

複製程式碼
       /**
             * 此時會發出一條sql,將30個學生全部查詢出來
             */
            List<Student> ls = (List<Student>)session.createQuery("from Student")
                                .setFirstResult(
0).setMaxResults(30).list(); Iterator<Student> stus = ls.iterator(); for(;stus.hasNext();) { Student stu = (Student)stus.next(); System.out.println(stu.getName()); }
複製程式碼

如果通過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()獲得物件

複製程式碼
       /**
             * 如果使用iterator方法返回列表,對於hibernate而言,它僅僅只是發出取id列表的sql
             * 在查詢相應的具體的某個學生資訊時,會發出相應的SQL去取學生資訊
             * 這就是典型的N+1問題
             * 存在iterator的原因是,有可能會在一個session中查詢兩次資料,如果使用list每一次都會把所有的物件查詢上來
             * 而是要iterator僅僅只會查詢id,此時所有的物件已經儲存在一級快取(session的快取)中,可以直接獲取
             
*/ Iterator<Student> stus = (Iterator<Student>)session.createQuery("from Student") .setFirstResult(0).setMaxResults(30).iterate(); for(;stus.hasNext();) { Student stu = (Student)stus.next(); System.out.println(stu.getName()); }
複製程式碼

在執行完上述的測試用例後,我們來看看控制檯的輸出,看會發出多少條 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提供的一級快取:

複製程式碼
       /**
             * 此時會發出一條sql,將所有學生全部查詢出來,並放到session的一級快取當中
             * 當再次查詢學生資訊時,會首先去快取中看是否存在,如果不存在,再去資料庫中查詢
             * 這就是hibernate的一級快取(session快取)
             */
            List<Student> stus = (List<Student>)session.createQuery("from Student")
                                    .setFirstResult(0).setMaxResults(30).list();
            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去查資料庫

複製程式碼
     try
        {
            session = HibernateUtil.openSession();
            
            /**
             * 此時會發出一條sql,將所有學生全部查詢出來,並放到session的一級快取當中
             * 當再次查詢學生資訊時,會首先去快取中看是否存在,如果不存在,再去資料庫中查詢
             * 這就是hibernate的一級快取(session快取)
             */
            List<Student> stus = (List<Student>)session.createQuery("from Student")
                                    .setFirstResult(0).setMaxResults(30).list();
            Student stu = (Student)session.load(Student.class, 1);
            System.out.println(stu.getName() + "-----------");
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        finally
        {
            HibernateUtil.close(session);
        }
        /**
         * 當session關閉以後,session的一級快取也就沒有了,這時就又會去資料庫中查詢
         */
        session = HibernateUtil.openSession();
        Student stu = (Student)session.load(Student.class, 1);
        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配置檔案中配置我們二級快取的一些屬性:

     <!-- 開啟二級快取 -->
        <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.1.7版本,如果是使用hibernate3的版本的話,那麼二級快取的提供類則要配置成這個:

<!--這個類在4.0版本以後已經不建議被使用了-->
<
property name="hibernate.cache.provider_class">net.sf.ehcache.hibernate.EhCacheProvider</property>

3.配置hibernate的二級快取是通過使用 ehcache的快取包,所以我們需要建立一個 ehcache.xml 的配置檔案,來配置我們的快取資訊,將其放到專案根目錄下

複製程式碼
<ehcache>

    <!-- Sets the path to the directory where cache .data files are created.

         If the path is a Java System Property it is replaced by
         its value in the running VM.

         The following properties are translated:
         user.home - User's home directory
         user.dir - User's current working directory
         java.io.tmpdir - Default temp file path -->
  
  <!--指定二級快取存放在磁碟上的位置--> <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="com.xiaoluo.bean.Student" maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk="true" /> <cache name="sampleCache2" maxElementsInMemory="1000" eternal="true" timeToIdleSeconds="0" timeToLiveSeconds="0" overflowToDisk="false" /> --> </ehcache>
複製程式碼

4.開啟我們的二級快取

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

複製程式碼
<hibernate-mapping package="com.xiaoluo.bean">
    <class name="Student" table="t_student">
        <!-- 二級快取一般設定為只讀的 -->
        <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="sex" column="sex" type="string"></property>
        <many-to-one name="room" column="rid" fetch="join"></many-to-one>
    </class>
</hibernate-mapping>
複製程式碼

二級快取的使用策略一般有這幾種:read-only、nonstrict-read-write、read-write、transactional。注意:我們通常使用二級快取都是將其配置成 read-only ,即我們應當在那些不需要進行修改的實體類上使用二級快取,否則如果對快取進行讀寫的話,效能會變差,這樣設定快取就失去了意義。

②如果使用annotation配置,我們需要在Student這個類上加上這樣一個註解:

複製程式碼
@Entity
@Table(name="t_student")
@Cache(usage=CacheConcurrencyStrategy.READ_ONLY)  //  表示開啟二級快取,並使用read-only策略
public class Student
{
    private int id;
    private String name;
    private String sex;
    private Classroom room;
    .......
}
複製程式碼

這樣我們的二級快取配置就算完成了,接下來我們來通過測試用例測試下我們的二級快取是否起作用

①二級快取是sessionFactory級別的快取

TestCase1:

複製程式碼
public class TestSecondCache
{
    @Test
    public void testCache1()
    {
        Session session = null;
        try
        {
            session = HibernateUtil.openSession();

            Student stu = (Student) session.load(Student.class, 1);
            System.out.println(stu.getName() + "-----------");
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        finally
        {
            HibernateUtil.close(session);
        }
        try
        {
            /**
             * 即使當session關閉以後,因為配置了二級快取,而二級快取是sessionFactory級別的,所以會從快取中取出該資料
             * 只會發出一條sql語句
             */
            session = HibernateUtil.openSession();
            Student stu = (Student) session.load(Student.class, 1);
            System.out.println(stu.getName() + "-----------");
            /**
             * 因為設定了二級快取為read-only,所以不能對其進行修改
             */
            session.beginTransaction();
            stu.setName("aaa");
            session.getTransaction().commit();
        }
        catch (Exception e)
        {
            e.printStackTrace();
            session.getTransaction().rollback();
        }
        finally
        {
            HibernateUtil.close(session);
        }
    }
複製程式碼
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:

複製程式碼
  @Test
    public void testCache2()
    {
        Session session = null;
        try
        {
            session = HibernateUtil.openSession();

            /**
             * 注意:二級快取中快取的僅僅是物件,而下面這裡只儲存了姓名和性別兩個欄位,所以 不會被載入到二級快取裡面
             */
            List<Object[]> ls = (List<Object[]>) session
                    .createQuery("select stu.name, stu.sex from Student stu")
                    .setFirstResult(0).setMaxResults(30).list();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        finally
        {
            HibernateUtil.close(session);
        }
        try
        {
            /**
             * 由於二級快取快取的是物件,所以此時會發出兩條sql
             */
            session = HibernateUtil.openSession();
            Student stu = (Student) session.load(Student.class, 1);
            System.out.println(stu);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
複製程式碼
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:

複製程式碼
  @Test
    public void testCache3()
    {
        Session session = null;
        try
        {
            session = HibernateUtil.openSession();
            /**
             * 將查詢出來的Student物件快取到二級快取中去
             */
            List<Student> stus = (List<Student>) session.createQuery(
                    "select stu from Student stu").list();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        finally
        {
            HibernateUtil.close(session);
        }
        try
        {
            /**
             * 由於學生的物件已經快取在二級快取中了,此時再使用iterate來獲取物件的時候,首先會通過一條
             * 取id的語句,然後在獲取物件時去二級快取中,如果發現就不會再發SQL,這樣也就解決了N+1問題 
             * 而且記憶體佔用也不多
             */
            session = HibernateUtil.openSession();
            Iterator<Student> iterator = session.createQuery("from Student")
                    .iterate();
            for (; iterator.hasNext();)
            {
                Student stu = (Student) iterator.next();
                System.out.println(stu.getName());
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }
複製程式碼

當我們如果需要查詢出兩次物件的時候,可以使用二級快取來解決N+1的問題。

④二級快取會快取 hql 語句嗎?

TestCase4:

複製程式碼
  @Test
    public void testCache4()
    {
        Session session = null;
        try
        {
            session = HibernateUtil.openSession();
            List<Student> ls = session.createQuery("from Student")
                    .setFirstResult(0).setMaxResults(50).list();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        finally
        {
            HibernateUtil.close(session);
        }
        try
        {
            /**
             * 使用List會發出兩條一模一樣的sql,此時如果希望不發sql就需要使用查詢快取
             */
            session = HibernateUtil.openSession();
            List<Student> ls = session.createQuery("from Student")
                    .setFirstResult(0).setMaxResults(50).list();
            Iterator<Student> stu = ls.iterator();
            for(;stu.hasNext();)
            {
                Student student = stu.next();
                System.out.println(student.getName());
            }
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
        finally
        {
            HibernateUtil.close(session);
        }
    }
複製程式碼
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中加入一條配置即可:

     <!-- 開啟查詢快取 -->
        <property name="hibernate.cache.use_query_cache">true</property>

然後我們如果在查詢hql語句時要使用查詢快取,就需要在查詢語句後面設定這樣一個方法:

List<Student> ls = session.createQuery("from Student where name like ?")
                    .setCacheable(true)  //開啟查詢快取,查詢快取也是SessionFactory級別的快取
                    .setParameter(0, "%王%")
                    .setFirstResult(0).setMaxResults(50).list();

如果是在annotation中,我們還需要在這個類上加上這樣一個註解:@Cacheable

接下來我們來通過測試用例來看看我們的查詢快取

①查詢快取也是sessionFactory級別的快取

TestCase1:

複製程式碼
  @Test
    public void test2() {
        Session session = null;
        try {
            /**
             * 此時會發出一條sql取出所有的學生資訊
             */
            session = HibernateUtil.openSession();
            List<Student> ls = session.createQuery("from Student")
                    .setCacheable(true)  //開啟查詢快取,查詢快取也是sessionFactory級別的快取
                    .setFirstResult(0).setMaxResults(50).list();
            Iterator<Student> stus = ls.iterator();
            for(;stus.hasNext();) {
                Student stu = stus.next();
                System.out.println(stu.getName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            HibernateUtil.close(session);
        }
        try {
            /**
             * 此時會發出一條sql取出所有的學生資訊
             */
            session = HibernateUtil.openSession();
            List<Student> ls = session.createQuery("from Student")
                    .setCacheable(true)  //開啟查詢快取,查詢快取也是sessionFactory級別的快取
                    .setFirstResult(0).setMaxResults(50).list();
            Iterator<Student> stus = ls.iterator();
            for(;stus.hasNext();) {
                Student stu = stus.next();
                System.out.println(stu.getName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {