lazy延遲加載
lazy(延遲加載)策略可用於<class>標簽,<property>標簽,集合(<set>/<list>)標簽以及<one-to-one>/<many-to-one>標簽上
<class>
class標簽中的lazy可選屬性為true/false,默認為ture,代表默認使用延遲加載策略
1 public static void main(String[] args) { 2 Session session = HibernateFactory.currentSession(); 3 Transaction tx = session.beginTransaction(); 4 5 Student student = (Student) session.load(Student.class, 1); 6 7 tx.commit(); 8 HibernateFactory.closeSession(); 9 10 System.out.println(student.getName()); 11 }
以上代碼在Session范圍內load了一個Student對象,此時Hibernate不會立即執行查詢student表的select語句,僅僅返回Student類的CGLIB代理類的實例,這個代理類實例有以下特征:
1:由Hibernate在運行時動態生成,繼承了Student類的所有屬性和方法。
2:當Hibernate創建Student代理類實例時,僅僅初始化了它的OID屬性,其他屬性都為null。
3:當應用程序第一次訪問Student代理類實例時(例如調用student.getXXX()或student.setXXX()方法),Hibernate會初始化代理類實例,執行select語句,從數據庫中加載Student對象的所有數據。但應用程序訪問Student代理類實例的getId()方法時,Hibernate不會初始化代理類實例,因為在創建代理類實例時OID就存在了,不必到數據庫中去查詢。
4:如果在Session范圍內沒有訪問Student代理類實例,而是在Session關閉后訪問了代理類實例,那么就會拋出"could not initialize proxy - no Session"的異常。
所以上訴代碼會報錯,如果想要代碼正常運行,有以下四種修改方法
1:將<class>中lazy屬性設置未false,表示不使用延遲加載策略,當Session調用load()函數時會立刻從數據庫中select出student的數據
2:在Session范圍內訪問一下student對象(例如調用student.getName())
3:使用get()方法代替load()方法,get()方法執行的時候會立即向數據庫發出查詢語句
4:使用Hibernate的API initialize來初始化代理類實例,代碼如下
1 public static void main(String[] args) { 2 Session session = HibernateFactory.currentSession(); 3 Transaction tx = session.beginTransaction(); 4 5 Student student = (Student) session.load(Student.class, 1); 6 if(!Hibernate.isInitialized(student)) { 7 Hibernate.initialize(student); 8 } 9 10 tx.commit(); 11 HibernateFactory.closeSession(); 12 13 System.out.println(student.getName()); 14 }
<property>
property標簽中的lazy可選屬性為true/false,這個特性需要類增強。
集合(set/list等)
集合標簽中的lazy可選屬性為true/false/extra,默認為true,代表只有在調用這個集合獲取里面的元素對象時,才發出查詢語句,加載其集合元素的數據。
首先插入數據到grade和student表中
public class HibernateTest { public static void main(String[] args) { Session session = HibernateFactory.currentSession(); Transaction tx = session.beginTransaction(); Grade grade = new Grade(); grade.setName("grade1"); Student student1 = new Student(); student1.setName("student1"); student1.setGrade(grade); Student student2 = new Student(); student2.setName("student2"); student2.setGrade(grade); grade.getStudents().add(student1); grade.getStudents().add(student2); session.save(grade); tx.commit(); HibernateFactory.closeSession(); } }
測試程序取出grade以及他對應的student列表(hbm文件中關閉了<class>級別的延遲加載)
1 public class HibernateTest { 2 public static void main(String[] args) { 3 Session session = HibernateFactory.currentSession(); 4 Transaction tx = session.beginTransaction(); 5 6 Grade grade = (Grade)session.load(Grade.class, 1); 7 8 tx.commit(); 9 HibernateFactory.closeSession(); 10 System.out.println(grade.getStudents()); 11 } 12 }
以上代碼在Session范圍中load了一個Grade對象,因為並沒有對<class>進行延遲加載,所以從數據庫中取到了grade的數據,但在session范圍外去獲取grade的集合屬性時程序缺拋出異常(
failed to lazily initialize a collection of role),因為Hibernate對集合屬性進行了延遲加載,如果想要代碼正常運行,有以下三種修改方法
1:在集合標簽<set>上設置lazy為false,取消集合的延遲加載,但這樣每次都會把set中的數據全部取出來,占用了大量內存,影響程序性能
2:在集合標簽<set>上設置fetch屬性為"join",這個屬性在后面fetch屬性介紹時會再提到
3:使用hql語句加載grade數據,hql中顯式的對兩個對象進行連接,代碼如下
1 public class HibernateTest { 2 public static void main(String[] args) { 3 Session session = HibernateFactory.currentSession(); 4 Transaction tx = session.beginTransaction(); 5 6 7 Query query = session.createQuery("from Grade as grade left outer join fetch grade.students where grade.id=:id"); 8 query.setParameter("id", 1); 9 Grade grade = (Grade) query.uniqueResult(); 10 11 tx.commit(); 12 HibernateFactory.closeSession(); 13 System.out.println(grade.getStudents()); 14 } 15 }
在集合的lazy屬性中有一個值為extra,這是一種比較聰明的延遲加載策略,即調用集合的size/contains等方法的時候,hibernate並不會去加載整個集合的數據,而是發出一條"聰明"的SQL語句以便獲得需要的值(例如通過sql中的count語句獲取集合的size),只有在真正需要用到這些集合元素對象數據的時候,才去發出查詢語句加載所有對象的數據。
<many-to-one>
<many-to-one>標簽中的lazy可選屬性為false/proxy/no-proxy,默認屬性為proxy
public class HibernateTest { public static void main(String[] args) { Session session = HibernateFactory.currentSession(); Transaction tx = session.beginTransaction(); Student student = (Student)session.load(Student.class,1); tx.commit(); HibernateFactory.closeSession(); System.out.println(student.getName()); System.out.println(student.getGrade().getName()); } }
以上代碼會報異常,因為lazy的默認屬性為proxy,作用與lazy="true"相似,啟用延遲加載,而在Session范圍外去取grade的值會出錯,如果要使程序正確執行,有以下3種方法
1:lazy="false",關閉延遲加載
2:在session范圍內訪問grade對象或者對其初始化
1 public class HibernateTest { 2 public static void main(String[] args) { 3 Session session = HibernateFactory.currentSession(); 4 Transaction tx = session.beginTransaction(); 5 6 Student student = (Student)session.load(Student.class,1); 7 if(!Hibernate.isInitialized(student.getGrade())) { 8 Hibernate.initialize(student.getGrade()); 9 } 10 tx.commit(); 11 HibernateFactory.closeSession(); 12 System.out.println(student.getName()); 13 System.out.println(student.getGrade().getName()); 14 } 15 }
3:配置Grade.hbm.xml,把它<class>標簽中的lazy設置為false,這樣不管<many-to-one>標簽中lazy的值是什么,都會立刻加載grade對象
<many-to-one>的lazy標簽還有一個取值為no-proxy,它和proxy效果一樣,不過proxy對象不是動態,是在編譯的過程中就創建的,需要進行特定的編譯
<one-to-one>
<one-to-one>與<many-to-one>基本一致,標簽中的lazy可選屬性為false/proxy/no-proxy,默認屬性為proxy,但是<one-to-one>中有一個屬性為constrained,一旦設置為false(這也是它的默認值),不管lazy設置為何值,Hibernate都會采取預先抓取。
fetch抓取策略
<many-to-one>/<one-to-one>
<many-to-one>/<one-to-one>標簽上fetch的可選取值有select/join,默認為select
fetch = "select"是在查詢的時候先查詢出一端的實體,然后在根據一端的查詢出多端的實體,會產生1+n條sql語句;
fetch = "join"是在查詢的時候使用外連接進行查詢,不會差生1+n的現象。此時lazy會失效
集合標簽
集合標簽(例如<set>/<list>)上fetch的可選取值有select/join/subselect,默認為select
1 public class HibernateTest { 2 public static void main(String[] args) { 3 Session session = HibernateFactory.currentSession(); 4 Transaction tx = session.beginTransaction(); 5 6 Grade grade = (Grade)session.load(Grade.class,1); 7 System.out.println(grade.getStudents()); 8 9 tx.commit(); 10 HibernateFactory.closeSession(); 11 } 12 }
當設置為select時,<set name="students" inverse="true" cascade="all" fetch="select">
輸出的sql語句為:
1 Hibernate: 2 select 3 grade0_.id as id1_0_0_, 4 grade0_.name as name2_0_0_ 5 from 6 grade grade0_ 7 where 8 grade0_.id=? 9 10 Hibernate: 11 select 12 students0_.gradeid as gradeid3_0_0_, 13 students0_.id as id1_1_0_, 14 students0_.id as id1_1_1_, 15 students0_.name as name2_1_1_, 16 students0_.gradeid as gradeid3_1_1_ 17 from 18 student students0_ 19 where 20 students0_.gradeid=?
先查詢出一端的實體,當你真正訪問關聯關系的時候,才會執行第二條select語句抓取當前對象的關聯實體或集合。
當設置為join時,<set name="students" inverse="true" cascade="all" fetch="join">
輸出的sql語句為:
1 Hibernate: 2 select 3 grade0_.id as id1_0_0_, 4 grade0_.name as name2_0_0_, 5 students1_.gradeid as gradeid3_0_1_, 6 students1_.id as id1_1_1_, 7 students1_.id as id1_1_2_, 8 students1_.name as name2_1_2_, 9 students1_.gradeid as gradeid3_1_2_ 10 from 11 grade grade0_ 12 left outer join 13 student students1_ 14 on 15 grade0_.id=students1_.gradeid 16 where 17 grade0_.id=?
查詢的時候使用外連接進行查詢。
當設置為subselect時,<set name="students" inverse="true" cascade="all" fetch="subselect">
使用新的測試程序
1 public class HibernateTest { 2 public static void main(String[] args) { 3 Session session = HibernateFactory.currentSession(); 4 Transaction tx = session.beginTransaction(); 5 6 List<Grade> grades = session.createQuery("from Grade").list(); 7 System.out.println(grades.get(0).getStudents().size()); 8 9 tx.commit(); 10 HibernateFactory.closeSession(); 11 System.out.println(grades.get(1).getStudents()); 12 } 13 }
輸出的sql語句為:
1 Hibernate: 2 select 3 grade0_.id as id1_0_, 4 grade0_.name as name2_0_ 5 from 6 grade grade0_ 7 8 Hibernate: 9 select 10 students0_.gradeid as gradeid3_0_1_, 11 students0_.id as id1_1_1_, 12 students0_.id as id1_1_0_, 13 students0_.name as name2_1_0_, 14 students0_.gradeid as gradeid3_1_0_ 15 from 16 student students0_ 17 where 18 students0_.gradeid 19 in 20 (select grade0_.id from grade grade0_)
另外發送一條SELECT 語句抓取在前面查詢到(或者抓取到)的所有實體對象的關聯集合. 這個理解起來有點糊塗, 舉個例子 : 如果你使用 Query 查詢出了2個Grade 實體, 由於開啟了懶加載,那么他們的 students 都沒有被初始化, 此時手動初始化一個Grade 的 students,Hibernate 會將前面查詢到的實體對象(2個Grade)的關聯集合使用一條 Select 語句一次性抓取回來, 這樣減少了與數據庫的交互次數, 一次將每個對象的集合都給初始化了;(他是將上一次查詢的 SQL 語句作為這一次查詢的 SQL語句的 where 子查詢, 所以上次查詢到幾個對象,那么這次就初始化幾個對象的集合)。
分析上面的代碼可以發現,在session范圍外訪問了grades.get(1).getStudents();因為使用的時"subselect"抓取策略,在訪問grades.get(0).getStudents()時會把之前查詢到的實體的關聯集合全部一次性抓取回來,所以程序沒有報錯;但如果使用"select"抓取策略的話,由於session范圍內沒有訪問grades.get(1).getStudents();所以在session范圍外訪問它會報錯,這就是select和subselect的區別。
batch-size批量加載屬性
batch-size可以看成是"select"和"subselect"的折中策略,既不想一次只加載一個實體的關聯集合,也不想一次加載所有實體的關聯結合,可以配合使用"select"和"batch-size",具體使用方法如下:
首先數據庫中的數據如下


Grade.hbm.xml的關鍵配置如下
1 <set name="students" inverse="true" cascade="all" fetch="select" batch-size="2"> 2 <key column="gradeid"/> 3 <one-to-many class="com.zlt.hibernatedemo.Student"/> 4 </set>
測試代碼
1 public class HibernateTest { 2 public static void main(String[] args) { 3 Session session = HibernateFactory.currentSession(); 4 Transaction tx = session.beginTransaction(); 5 6 List<Grade> grades = session.createQuery("from Grade").list(); 7 System.out.println(grades.get(0).getStudents().size()); 8 9 tx.commit(); 10 HibernateFactory.closeSession(); 11 System.out.println(grades.get(1).getStudents()); 12 } 13 }
輸出的sql語句
1 Hibernate: 2 select 3 grade0_.id as id1_0_, 4 grade0_.name as name2_0_ 5 from 6 grade grade0_ 7 8 Hibernate: 9 select 10 students0_.gradeid as gradeid3_0_1_, 11 students0_.id as id1_1_1_, 12 students0_.id as id1_1_0_, 13 students0_.name as name2_1_0_, 14 students0_.gradeid as gradeid3_1_0_ 15 from 16 student students0_ 17 where 18 students0_.gradeid in (?, ?)
結果分析:
以上代碼在Session范圍內抓取了grades.get(0).getStudents(),而在Session范圍外訪問了grades.get(1).getStudents(),由於使用的是"select"抓取策略而不是"subselect",所以應該報異常,但是程序卻正確執行,而且確實抓取到了grades.get(1).getStudents(),這是因為設置了batch-size="2",Hibernate 使用一條 Select 語句一次性抓取2個grade實體對象的關聯集合回來(例子中前一次查詢共查詢出4個grade實體對象,如果使用"subselect"抓取策略會一次性抓取這4個實體對象的關聯集合),所以可以發現sql語句中的第二條用了in。
當抓取grades.get(0).getStudents()時,會一次抓取兩個關聯集合,即grades.get(0)和grades.get(1),此時在Session范圍外訪問grades.get(2)和grades.get(3)會報錯
當抓取grades.get(1).getStudents()時,會一次抓取兩個關聯集合,即grades.get(1)和grades.get(2),此時在Session范圍外訪問grades.get(0)和grades.get(3)會報錯
當抓取grades.get(2).getStudents()時,會一次抓取兩個關聯集合,即grades.get(2)和grades.get(3),此時在Session范圍外訪問grades.get(0)和grades.get(1)會報錯
當抓取grades.get(3).getStudents()時,會一次抓取兩個關聯集合,即grades.get(3)和grades.get(0),此時在Session范圍外訪問grades.get(1)和grades.get(2)會報錯
fetch與lazy組合情況
1、當lazy="true" fetch="select" 的時候,這個時候是使用了延遲策略,開始只查詢出一端實體,多端的不會查詢,只有當用到的時候才會發出sql語句去查詢。
2、當lazy="false" fetch = "select" 的時候,個時候是使沒有用延遲策略,同時查詢出一端和多端,同時產生1+n條sql。
3、當fetch = "join"的時候,不管lazy設置為什么,這個時候延遲已經沒有什么用了,因為采用的是外連接查詢,同時把一端和多端都查詢出來了。
