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"
"https://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 撰寫的 |

Java 學習之路


