在項目中遇到的趣事
本文基于hibernate緩存機制與N+1問題展開思考,
先介紹何為N+1問題
再hibernate中用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 }
控制臺輸出:
1 Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.rid as rid2_, student0_.sex as sex2_ from t_student student0_ limit ?
如果通過list()方法來獲得對象,毫無疑問,hibernate會發出一條sql語句,將所有的對象查詢出來,這沒什么問題。
用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()這種形式呢? 嗯..............存在即合理。想想有沒有特殊的情況能發揮?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);
我們來看看控制臺輸出:
?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() + "-----------");
1 Hibernate: select student0_.id as id2_, student0_.name as name2_, student0_.sex as sex2_, student0_.rid as rid2_ from t_student student0_ limit ? 2 3 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級別)(簡單介紹)
如果我們只是取出對象的一些屬性的話,則不會將其保存到二級緩存中去,因為二級緩存緩存的僅僅是對象。
由于學生對象已經緩存在二級緩存中了,此時再使用iterate來獲取對象的時候,首先會通過一條取id的語句,
然后在獲取去二級緩存中對象時,如果發現就不會再發SQL,這樣也就解決了N+1問題 而且內存占用也不多。
萬千叢中一點綠, 對象!,對象!,對象! 二級緩存就把第一次對象查詢攔截了,解決了N+1問題
?