
Hibernate 的 Lazy Loading 全攻略
對 Hibernate 的使用者來說,應該每個人都看過類似的訊息: org.apache.catalina.core.StandardWrapperValve invoke SEVERE: Servlet.service() for servlet [RegionController] in context with path [/HibernateTransaction] threw exception [An exception occurred processing JSP page /regions.jsp at line 40 37: </td> 38: <td> 39: <input type="text" name="regionName" size="50" 40: value="<%= region.getRegionName() %>"> 41: </td> 42: </tr> 43: </tbody> Stacktrace:] with root cause org.hibernate.LazyInitializationException: could not initialize proxy - no Session at org.hibernate.proxy.AbstractLazyInitializer.initialize(...) ... at java.lang.Thread.run(Thread.java:722) 裡頭的 原因Hibernate 預設會使用所謂的 Lazy Loading 機制。也就是說,有些欄位的內容,比方說 BLOB 型別這種資料量比較大的欄位,或是 Association 做出來的欄位,並不會在一開始 DAO 取得資料的時候就載入,而是等到 View 真正想用到這些欄位資料的時候才載入。為了做出這個效果,DAO 一開始透過 Hibernate 所回傳的物件,通常不是原來設計的 Persistent Object,而是 Persistent Object 的 Proxy。如果我們在 View 裡頭呼叫這個 Proxy 物件的 Getter 方法,想要使用欄位的內容,Hibernate 才會讓 Proxy 物件透過 Hibernate 的 一般會去取得 Persistent Object 的地方通常都是在 DAO,如果這時候需要讀取欄位資料做處理,因為 Hibernate 的 借用 Guide to Java Persistence and Hibernate 網站 Short Life of a Session 這張 Sequence Diagram 來做說明的話,就是在步驟 8 JSP Rendering 的時候,Proxy 物件需要透過 Hibernate 的 有一點要釐清的是,Lazy Loading 這個問題並不是 Hibernate 獨有,事實上很多 Object/Relational Mapping (ORM) 的程式庫,為了要爭取一些執行上的效率、降低記憶體空間與網路流量的負荷,幾乎都採用類似的作法。換句話說,想要學好任何一種 ORM,怎麼處理 Lazy Loading 都是必須面對的重要議題。 想法知道原因之後,有沒有一些想法,可能有機會解決問題? 有,而且超白爛的:
作法有了想法之後,接下來才是從 Hibernate 的組態設定或是 API 裡頭,去找出可行的作法,實現你的想法。 常見的作法有底下四種,分成兩大類:
修改 Mapping 檔案最直接的作法,就是把 Lazy Loading 機制關掉,不就結了? 沒錯,所以解決 Lazy Loading 的方法之一,就是修改 Persistent Object 的 Mapping 檔案,關閉 Lazy Loading 機制,讓 Hibernate 強迫 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class lazy="false" catalog="codedata" table="Region" name="tw.com.codedata.model.Region"> ... </class> </hibernate-mapping> 這種作法的好處是超級方便,缺點則是可能會因此花比較多的執行時間、記憶體空間、與網路流量處理資料。會發生 Lazy Loading 問題的欄位,通常是 1-1、1-n、n-m 的 Association 欄位,因此衍生出來的資料量會很可怕。 結論就是,如果不是 Hello World 或 Proof of Concept 型態的專案,還是不要這樣做比較好。 修改 DAO 程式寫法如果靜態地、全面性地在 Mapping 檔案裡面加上 可以,Hibernate 提供了一個類別,剛好就叫做
所以,解決 Lazy Loading 的方法之二,就是修改 DAO 的各個 CRUD 方法,透過 public class RegionDAO { private SessionFactory factory = HibernateUtil.getSessionFactory(); public void setSessionFactory(SessionFactory factory) { this.factory = factory; } public Region findByPrimaryKey(int regionId) { Region region = null; Session session = factory.getCurrentSession(); Transaction tx = null; try { tx = session.beginTransaction(); region = (Region) session.load(Region.class, regionId); Hibernate.initialize(region); tx.commit(); } catch (Exception ex) { if (tx != null) tx.rollback(); System.out.println(ex.getMessage()); } return region; } } 這種作法看起來似乎很有彈性,但是這個彈性其實沒什麼太大意義。因為 DAO 基本上提供的是比較一般化的功能,在 DAO 裡面是沒辦法預測 View 會怎麼使用它傳回的 Persistent Object。即使我們對每個 DAO 方法都提供兩種版本,一種呼叫 結論就是,第二種作法感覺起來比第一種作法有彈性,但是實際上是半斤八兩。 自行撰寫 Filter 處理提早塞好資料這些作法,雖然簡單,但是都不是很好的作法。那有沒有辦法延後關閉 Hibernate 的 借用 Guide to Java Persistence and Hibernate 網站 Lifetime Until the View is Rendered 這張 Sequence Diagram 來做說明的話,就是透過 Filter 的協助,在步驟 9 JSP Rendering 的時候, 所以解決 Lazy Loading 的方法之三,就是自己寫個 Filter,採用 Session per Request 的方式,控制 Hibernate 的 public class LazyLoadingFilter implements Filter { private SessionFactory factory = null; private FilterConfig config = null; public void destroy() {} public void init(FilterConfig config) throws ServletException { this.config = config; factory = HibernateUtil.getSessionFactory(); } public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Transaction tx = null; try { tx = factory.getCurrentSession().beginTransaction(); chain.doFilter(request, response); tx.commit(); } catch (Exception ex) { System.out.println(ex.getMessage()); tx.rollback(); } } }
<?xml version="1.0" encoding="UTF-8"?> <web-app ...> <filter> <filter-name>LazyLoadingFilter</filter-name> <filter-class>tw.com.codedata.helper.LazyLoadingFilter</filter-class> </filter> <filter-mapping> <filter-name>LazyLoadingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app> 因為採用 Session per Request 的方式,所以必須調整 DAO 的寫法,把 Transaction 相關的程式碼拿掉。範例如下: public class RegionDAO { private SessionFactory factory = HibernateUtil.getSessionFactory(); public void setSessionFactory(SessionFactory factory) { this.factory = factory; } public Region findByPrimaryKey(int regionId) { Session session = factory.getCurrentSession(); return (Region) session.load(Region.class, regionId); } } 這種作法看起來辛苦一點,因為還要自己寫個 Filter,並且要宣告。不過好處就是,這種作法只有在真正需要用到資料的時候,才會透過 Hibernate 的 透過 Spring Framework 協助Spring Framework 對「不要重複發明輪子」這件事還蠻堅持的,既然每個人每個專案用到 Hibernate 都需要處理 Lazy Loading 的問題,所以解決方法之四,就是不要每個人每個專案都得寫個 為什麼叫做 使用方式很簡單,第一點當然是透過 <?xml version="1.0" encoding="UTF-8"?> <beans ...> <jee:jndi-lookup id="dataSource" expected-type="javax.sql.DataSource" jndi-name="java:comp/env/jdbc/CodeDataDS" /> <bean id="codedataSessionFactory" class="org.springframework.orm.hibernate3.LocalSessionFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="configLocation"> <value>classpath:hibernate.cfg.xml</value> </property> </bean> <bean id="txManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager"> <property name="sessionFactory" ref="codedataSessionFactory" /> </bean> <tx:annotation-driven transaction-manager="txManager" /> <bean id="regionDAO" class="tw.com.codedata.model.RegionDAO"> <property name="sessionFactory" ref="codedataSessionFactory" /> </bean> <bean id="regionFacade" class="tw.com.codedata.model.RegionFacade"> <property name="regionDAO" ref="regionDAO" /> </bean> </beans> 第二點,因為是 Filter,當然一樣必須在 <?xml version="1.0" encoding="UTF-8"?> <web-app ...> <filter> <filter-name>OpenSessionInViewFilter</filter-name> <filter-class> org.springframework.orm.hibernate3.support.OpenSessionInViewFilter </filter-class> <init-param> <param-name>sessionFactoryBeanName</param-name> <param-value>codedataSessionFactory</param-value> </init-param> <init-param> <param-name>singleSession</param-name> <param-value>true</param-value> </init-param> <init-param> <param-name>flushMode</param-name> <param-value>AUTO</param-value> </init-param> </filter> <filter-mapping> <filter-name>OpenSessionInViewFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app> Filter 三個參數的意義分別是:
第三點,因為搭配使用 Spring Framework 提供的 @Transactional(isolation=Isolation.READ_COMMITTED, propagation=Propagation.REQUIRED) public class RegionFacade { private RegionDAO regionDAO = null; public void setRegionDAO(RegionDAO regionDAO) { this.regionDAO = regionDAO; } public Region getRegion(int regionId) { return regionDAO.findByPrimaryKey(int regionId); } } 第四點,DAO 取得 Hibernate public class RegionDAO { private SessionFactory factory = null; public void setSessionFactory(SessionFactory factory) { this.factory = factory; } public Region findByPrimaryKey(int regionId) { Session session = SessionFactoryUtils.getSession(factory, true); return (Region) session.load(Region.class, regionId); } } 第四種作法跟第三種作法在執行時間、記憶體空間、與網路流量這些問題上,應該是差不多,不過好處就是使用 Spring Framework 撰寫的 |