延遲初始(Lazy Initialization)


有時候您只是想要獲得物件中某個屬性的資料,如果您的物件中包括Set等容器物件,若從資料庫中載入資料時全部載入所有的物件,卻只是為了取得某個屬性, 顯然的這樣很沒有效率。

Set  中的範例來說,如果您只是想取得物件之後,顯示物件的某些屬性,例如id屬性:
Session session = HibernateUtil.getSessionFactory().openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getId());
session.close();

在這個例子中,name或email的資訊不必要從資料庫中載入,在Hibernate中支援延遲初始(Lazy onitialization),只有在真正需要物件中的屬性資料時,才從資料庫中取得資料,Hibernate預設會使用延遲加載的功能,使用load()方法載入實體物件時(get()方法不不會使用延遲初始),並不會載入屬性資料,也就是不會一開始就下SQL至資料庫中撈取對應的資料。

可以藉由映射文件中的lazy屬性來設定是否使用延遲初始,例如在映射文件中如下設定:
  • User.hbm.xml
<?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 name="onlyfun.caterpillar.User" table="user" lazy="false">
....
<set name="emails" table="email">
<key column="id"/>
<element column="address"/>
</set>
</class>

</hibernate-mapping>

由於lazy屬性被設定為false,延遲初始的功能被關閉,所以上面的程式會使用以下的SQL來查詢:
Hibernate: select user0_.id as id0_, user0_.name as name0_0_ from user user0_ where user0_.id=?
Hibernate: select emails0_.id as id0_, emails0_.address as address0_ from email emails0_ where emails0_.id=?

除了所有的屬性資料之外,屬性中的所有的容器物件之資料也一併被查詢了,即使程式中還不會使用到容器中的物件資訊。也可以單獨對集合物件進行設定,例如:
  • User.hbm.xml
<?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 name="onlyfun.caterpillar.User" table="user">
....
<set name="emails" table="email" lazy="false">
<key column="id"/>
<element column="address"/>
</set>
</class>

</hibernate-mapping>


在啟用延遲初始的情況下,如果如下查詢資料:
Session session = HibernateUtil.getSessionFactory().openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getName());
Iterator iterator = user.getEmails().iterator();
while(iterator.hasNext()) {
    System.out.println(iterator.next());
}
session.close();


在開啟SQL顯示的情況下,會顯示以下的內容:
Hibernate: select user0_.id as id0_, user0_.name as name0_0_ from user user0_ where user0_.id=?
caterpillar
Hibernate: select emails0_.id as id0_, emails0_.address as address0_ from email emails0_ where emails0_.id=?
caterpillar.onlyfun@yahoo.com
caterpillar.onlyfun@gmail.com


可以看到,只有在需要屬性或查詢容器中物件時,才會向資料庫索取資料。

使用延遲初始功能的好處之一,就是您可以只取用對應的屬性,而Hibernate只會針對相關屬性進行資料庫的SQL查詢,而另一個好處是,在只需要實體物件參考時,可以避免不必要的SQL查詢,例如:
Session session = HibernateUtil.getSessionFactory().openSession();
Transaction tx = session.beginTransaction();

User user = (User) session.load(User.class, new Integer(1));
Room room = new Room();
room.setUser(user);

session.save(room);

tx.commit();
session.close();


使用延遲初始時,由於在需要資料時會向資料庫進行查詢,所以session不能關閉,如果關閉會丟出 LazyInitializationException 例外,例如下面的程式就會丟出例外:
Session session = HibernateUtil.getSessionFactory().openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getName());

session.close();

Iterator iterator = user.getEmails().iterator();
while(iterator.hasNext()) {
    System.out.println(iterator.next());
}

如果您使用了延遲初始,而在某些時候仍有需要在session關閉之後取得相關物件,則可以使用Hibernate.initialize()來先行載入 相關物件,例如:
Session session = HibernateUtil.getSessionFactory().openSession();
User user = (User) session.load(User.class, new Integer(1));
System.out.println(user.getName());

Hibernate.initialize(user); // 先載入容器中的物件

session.close();

Iterator iterator = user.getEmails().iterator();
while(iterator.hasNext()) {
    System.out.println(iterator.next());
}

即使啟用延遲初始,在Hibernate.initialize()該行,email容器中的物件已經被載入,所以即使關閉session也無所謂了,這 種情況發生在某個情況下,您啟用了延遲初始,而使用者操作過程中,會在稍後一陣子才用到email容器,您不能浪費session在這段時間的等待上,所 以您先行載入容器物件,直接關閉session以節省資源。

實際上,Hibernate延遲初始的功能是使用代理物件,Hibernate會在執行時期產生實體物件的子類別,所以實體物件必須有公開的建構子,不得 有final的方法或為final類別,如果想要從load()之後的物件上取得實體物件的Class物件,則可以使用 HibernateProxyHelper.getClassWithoutInit除了所有的屬性資料之外,屬性中的所有的容器物件之資料也一併被查詢了,即使程式中還不會使用到容器中的物件資訊。ializingProxy()方法。

在某些時候,您只是想從一個集合的屬性上得知有多少數量的物件,例如呼叫size()、contains()或isEmpty()方法,預設上呼叫這些方法都會引發資料庫的查詢,您可以設定lazy為extra,例如:
<class name="onlyfun.caterpillar.User" table="t_user">
    ...
    <set name="emails" lazy="extra"...>
        ....
    </set>
</class>

如此呼叫集合物件的size()、contains()或isEmpty()方法,就不會引發資料庫SQL查詢,但如果是呼叫Map或List的containKey()、get()方法,則仍會進行資料庫查詢。