1. 程式人生 > >hibernate筆記: 關於懶載入和load()方法之三

hibernate筆記: 關於懶載入和load()方法之三

最近面試別人,正好出的筆試題中有道關於Hibernate延遲載入的問題,聊天過程中發現很多人對Hibernate的延遲載入有些理解誤區,寫些東東在這裡,希望對大家有所幫助。

首先是第一個誤區:延遲載入只能作用於關聯實體
看到這個是不是在想:非關聯實體延遲載入有什麼用?
為了解答上面這個問題,我們可以先考慮另一個問題:Hibernate Session的get和load方法有什麼區別?
如果你的回答是:當方法引數為資料庫不存在的id時,get會返回null,load會丟擲異常,那麼恭喜你,進入了第二個誤區
如果此時你還想補充一下:load會從快取中取出資料而get不會,再次恭喜,進入第三個誤區

如果你在上面三個誤區中有一個踏入了,那麼我敢打賭,你一定是被網上那些半吊子的工程師們寫的部落格給戕害了。。。。
此時是不是很憤怒?這些長久以來你牢記在心的Hibernate的特性原來都是浮雲。。。。

呵呵,接下來我們一個個來走出這些誤區。
Mop上無圖無真相,我們這裡無碼無真相——不要誤會,我是說程式碼

首先看看第二個誤區:當方法引數為資料庫不存在的id時,get會返回null,load會丟擲異常
如果你現在想說:沒錯啊,我自己就測試過,get確實返回了null,load確實丟擲了異常。
那麼請回答:load是在執行load語句時丟擲異常的嗎?為什麼?如果你答不上來,那麼接著看下面的程式碼吧:

  1. @Test(expected = IllegalArgumentException.class)  
  2. publicvoid 延遲載入() throws Exception {  
  3.     // 啟動
  4.     Session session = sessionFactory.openSession();  
  5.     Transaction tx = session.beginTransaction();  
  6.     User user = (User)session.load(User.class, 100L);  // 不存在的ID
  7.     try {  
  8.         user.getName();  
  9.     } catch (ObjectNotFoundException ex) {  
  10.         // 命中資料庫發現沒有物件即丟擲ObjectNotFoundException異常
  11.         thrownew IllegalArgumentException("隨便丟擲一個不可能的異常");  
  12.     }  
  13.     tx.commit();  
  14.     session.close();  
  15. }  

由這個test case我們可以知道load並不是在執行時就馬上丟擲不存在資料的異常的(ObjectNotFoundException),這是為什麼呢?再看程式碼:

  1. @Test(expected = IllegalArgumentException.class)  
  2. publicvoid 延遲載入() throws Exception {  
  3.     // 啟動
  4.     Session session = sessionFactory.openSession();  
  5.     Transaction tx = session.beginTransaction();  
  6.     User user = (User)session.load(User.class, 100L);  // 不存在的ID
  7.     Assert.assertTrue(user instanceof HibernateProxy);  
  8.     user.getId();  // 由於ID是不被延遲載入的屬性,因此不會丟擲異常
  9.     try {  
  10.         Hibernate.initialize(user);  // 此時才會觸發命中資料庫
  11.         //user.getName();
  12.     } catch (ObjectNotFoundException ex) {  
  13.         // 命中資料庫發現沒有物件即丟擲ObjectNotFoundException異常
  14.         thrownew IllegalArgumentException("隨便丟擲一個不可能的異常");  
  15.     }  
  16.     tx.commit();  
  17.     session.close();  
  18. }  

看高亮的幾行,程式碼已經把問題說得很清楚了,get和load最大的區別是(假設快取皆空的情況):get是立即命中資料庫去查詢這條記錄,而load則是直接返回一個代理物件(HibernateProxy)而不命中資料庫,換句話來說load是為單個物件進行了延遲載入,如果你不去訪問這個物件的除ID外的屬性,即使目標記錄不存在它也永遠都不會丟擲異常。由於load不立即命中資料庫,它確實有一定機率提高效率

OK,我想上面一段話應該可以解釋第一和第二個誤區了,那麼第三個誤區呢?
再看程式碼

  1. @Test
  2. publicvoid get和load一級快取測試() throws Exception {  
  3.     // 啟動
  4.     Session session = sessionFactory.openSession();  
  5.     Transaction tx = session.beginTransaction();  
  6.     // 驗證load在快取為空的情況下是否會使得載入的物件過一級快取
  7.     User user1 = (User)session.load(User.class, 1L);  // 存在的ID,此時雖然沒有解開Proxy但已經進入快取
  8.     Assert.assertTrue(user1 instanceof HibernateProxy);  
  9.     Hibernate.initialize(user1);  // 解開Proxy,會觸發命中資料庫操作
  10.     User user3 = (User)session.get(User.class, 1L);  
  11.     Assert.assertTrue(user3 instanceof HibernateProxy);  // 即使使用get,但由於快取中儲存的是一個Proxy,所以這裡得到的也是Proxy
  12.     Hibernate.initialize(user3);  // 解開Proxy,但不會命中資料庫
  13.     // 驗證在load一個不存在的ID後,不解開然後get
  14.     User user4 = (User)session.load(User.class, 100L);  // 不存在的ID,仍然將Proxy進入快取
  15.     Assert.assertTrue(user4 instanceof HibernateProxy);  
  16.     //Hibernate.initialize(user3);  // 不解開Proxy
  17.     try {  
  18.         session.get(User.class, 100L);  // 得到Proxy,命中資料庫嘗試解開Proxy,由於ID不存在因此丟擲異常
  19.         Assert.fail("ID不存在所以會出錯,不會執行本條");  
  20.     } catch (ObjectNotFoundException ex) {  
  21.     }  
  22.     // 清空快取
  23.     session.clear();  
  24.     // 驗證快取為空的情況下get是否為Proxy
  25.     User user6 = (User)session.get(User.class, 1L);  // 命中資料庫,直接將組裝完成的User實體進入快取
  26.     Assert.assertTrue(!(user6 instanceof HibernateProxy));  
  27.     // 驗證get從快取中取出物件
  28.     User user7 = (User)session.get(User.class, 1L);  
  29.     Assert.assertTrue(!(user7 instanceof HibernateProxy)); // 快取中是真實的User物件,get取出的就是真實的User物件
  30.     // 驗證load是否從一級快取取資料
  31.     User user8 = (User)session.load(User.class, 1L);  
  32.     Assert.assertTrue(!(user8 instanceof HibernateProxy));  // 快取中是真實的User物件,load取出的也是真實的User物件
  33.     tx.commit();  
  34.     session.close();  
  35. }  

相信註釋已經足夠詳細了,開啟hibernate.show_sql,總共命中三次資料庫(執行SQL),分別在高亮的三行處,其餘的全是從快取中取資料。
而且值得注意的一點是,如果物件是從load載入到快取中的,那麼不論get還是load獲取出來的都是一個Proxy,如果沒有被解開過,那麼get會嘗試解開它;如果物件是從get載入到快取中的,那麼load和get取出來都會是真實的實體物件。也就是說,get和load都會從快取中取出物件,且取出的物件總是保持其第一次載入時的狀態(load為Proxy,get為真實物件)

以上程式碼是一級快取的驗證,想驗證二級快取只需要從Hibernate中開啟二級快取再次執行程式碼即可