在Java使用Spring Framework或是單純使用Hibernate時,使用@OneToMany@ManyToMany來標注Entity Class非常方便,可以輕鬆處理外來鍵與外來資料表的問題。但偶爾當你使用getXXX()方法要讀取外來資料表的時候,可能會遇到Hibernate報錯說「org.hibernate.LazyInitializationException: failed to lazily initialize a collection」,本篇文章將解析為什麼會發生這種問題,以及要怎麼解決它。

問題分析

使用javax.persistence的API,也就是使用JPA的時候,在設定Entity時可以使用@OneToOne@OneToMany@ManyToOne@ManyToMany等註釋(Annotation)標註Field,讓JPA知道這個Field對應的Table Column是一個外來鍵,用起來非常方便。但有些時候透過外來鍵讀取另一個表單的資料時會出錯顯示以下內容。

org.hibernate.LazyInitializationException: failed to lazily initialize a collection

這個原因是不同的Mapping Annotation在資料庫select資料的時機是不一樣的,有些是預設使用FetchType.EAGER,代表我們索取A資料表的時候Framework就立刻幫我們把對應的外來B資料表也一起select。而有些Annotation預設是使用FetchType.LAZY,只有當我們實際呼叫getXXX()方法的時候Framework才去資料庫讀取資料。而問題出在設定為LAZY時,我們的程式要去讀取資料時資料庫連線已經關閉(或是被收回去給其他地方使用),沒辦法延遲取得資料。

這通常發生在透過DAO之類的方法獲得資料,卻過一段時間才使用它,或是離開原本的Spring Component才使用它。

下表是各種Annotation預設的Fetch Type。

AnnotationFetch Type
@OneToOneFetchType.EAGER
@ManyToOneFetchType.EAGER
@OneToManyFetchType.LAZY
@ManyToManyFetchType.LAZY

解決方式

推薦方式

使用@Transactional標註在會遇到此問題的Method上。

利用資料庫的交易(Transaction)機制,搭配Spring Framework的@Transactional註釋可以解決這個問題,Spring讓程式從進入Method到離開Method都確保其獨立完整性。如果我們只是讀取資料,不會修改資料的話,也可以像下方範例那樣加上readOnly = true

package tw.klab.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class KlabBlogService {
    @Autowired
    private KlabBlogPostDao postDao;

    @Transactional(readOnly = true)
    public void dosomething() {
        postDao.findById(...);
    }
}

需要注意@Transactional註釋是透過Spring的代理機制達成的,只有從Class外部呼叫的時候才會生效,例如上面範例中如果從KlabBlogService內部的另一個Mehtod去呼叫dosomething()的話,Transaction機制是不會生效的。

次要方式

在Entity Class中使用@OneToMany(fetch = FetchType.EAGER),讓JPA立刻索取資料。

這種方式可以解決要讀取資料時連線已經關閉的問題,但會帶來效能問題。如果對應的外來資料表的資料非常多的話,每次都必須大量的讀取不一定會用上的資料。但是開發者確定資料量不多、或是每次都會用上這些資料的話,將Fetch Type改為EAGER也是OK的。

package tw.klab.demo;

import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.ManyToOne;

@Entity
public class KlabBlogPost {
    @OneToMany(fetch = FetchType.EAGER)
    private List<BlogTag> tags;
}

延伸閱讀

Spring與JPA的Transactional差別

在現代的Spring Framework中,使用以下兩種Annotation都可以達成交易的需求,因為Spring遇到這兩種Annotation都會處理。

  • javax.transaction.Transactional
  • org.springframework.transaction.annotation.Transactional

但是Spring本身的Transactional Annotation多了一些功能,例如本文章上半部的readOnly = true就只有Spring的Transactional Annotation有提供。還有JPA的Annotation中rollbackFornoRollbackFor只接受Class型態的參數,而Spring版本的還可以接受String參數。

以下提供幾個Spring的Transactional才有的參數。

  • boolean readOnly
  • int timeout
  • String timeoutString
  • String[] rollbackForClassName
  • String[] noRollbackForClassName

Stackoverflow上的相關討論

https://stackoverflow.com/questions/11746499/how-to-solve-the-failed-to-lazily-initialize-a-collection-of-role-hibernate-ex

https://stackoverflow.com/questions/26387399/javax-transaction-transactional-vs-org-springframework-transaction-annotation-tr/62702146#62702146