---
# System prepended metadata

title: LazyInitializationException
tags: [Grails]

---

# :memo: <span style="font-weight:800">LazyInitializationException</span>

###### tags: `Grails`

在使用Grails開發的時候遇見這個問題兩次。同樣都是嘗試從session中取出PO後，取得PO nested data時候出現錯誤。第一次較快處理解決，但後面的就稍為難以處理。

第一次的問題比較簡單，大概是像以下處理方式：

假設每個 User 對應到一個 UserDetail

```groovy=1
class User {
    String firstName
    String lastName
    UserDetail userDetail
}
```

```groovy=1
class UserDetail {
    String phoneNumber
    String birthDate
}
```

在沒有設定`fetch = eager` or `(Lazy = false)`的情況下，不透過Hibernate Session只能取得前兩個一般型別的資訊。假設我們在使用者登入時，將使用者的PO從database取出並放入HttpSession。

```groovy=
User user = User.get(userId)
session.user = user
```
但如果其他的request再從HttpSession中拿取User nested object就會報錯。

```groovy=1
User user = session['user']
user.firstName //這段會正常

user.UserDetail.phoneNumber //會噴Exception

```

 <p style="font-size:32px;font-weight:800">Secret of object lifecycle with Hibernate Session</p> 

在 Grails 中對於Hibernate session 有此段[描述](https://docs.grails.org/4.0.10/ref/Domain%20Classes/attach.html)

![image alt](https://i.imgur.com/SRhBPtB.png)

簡單來說，每個request會取得一個新的session，在該請求中所有對資料庫操作的時候，都會共用該session 並在 request 結束時關閉。所以這時候如果存放在 HttpSession中的Domain Entity將會變成**Detached** 狀態

> 如果在該request中會觸及到多執行緒，就得另再取得新的 Hibernate Session 與該thread 做綁定，否則會出現Exception

我們可以透過 attach(), refresh(), merge() 等方法，將domain 重新回到Managed狀態。

```groovy=
user.attach()
user.userDetail.phoneNumber //此時就可以重新將物件連線使用
```

:::info
**LifeCycle of Entity (JPA)**
![](https://i.imgur.com/e8dc6Fm.png)
:::

## OpenSessionInView

不過上述的情況是在 Controller or Service 層的正常情況，一般在session.flush()後或是 request 結束 session 會是被關閉的。

```groovy=
def b = Book.get(1)
b.title = "Blah"
b.save(flush:true)

b.discard() //與Hibernate Session detach

...
if (!b.isAttached()) { //這時候isAttached()就會為false
    b.attach()
}
```

那這時候，假如我想在 view 層對取得物件內層的屬性，Lazyloading還能生效，就得歸功於[**OSIV(Open Session In View)**](https://www.baeldung.com/spring-open-session-in-view) 

OpenSessionInViewFilter主要是保持Session狀態直到頁面傳送到客戶端，這樣就可以解決LazyLoading帶來的問題。

**OSIV** 在 request 把 session 繫結到當前thread期間一直保持hibernate session在open狀態，使session在request的整個期間都可以使用，如在View層裡PO也可以lazy loading資料，如 ${ company.employees }。當View 層邏輯完成後，才會通過Filter的doFilter方法或Interceptor的postHandle方法自動關閉session。

> cons about **OSIV** ：
因為在生成頁面完成後，session才會被釋放，所以如果使用者的網路狀況比較差，那麼連線池中的連結會遲遲不被釋放，造成記憶體增加，系統性能受損。

在沒有使用Spring提供的 **OSIV** 情況下，因需要在service(or Dao)層裡把session關閉，所以lazy loading 為true的話，要在應用層內把關係集合都初始化，如 `company.getEmployees()`，否則Hibernat 會拋出 `session already closed Exception`。

**OSIV**提供了一種簡便的方法，較好地解決了lazy loading問題。在Springboot中有兩種配置方式 `OpenSessionInViewInterceptor` 和`OpenSessionInViewFilte` [(參考使用方法)](https://www.baeldung.com/spring-open-session-in-view)，功能相同，只是一個在web.xml配置，另一個在application.xml配置而已。

然而在**Grails**中，這些設定都幫你做好，預設在view層中，session都是開啟且未關閉的。

## siteMesh 踩雷

但最近呢，出現了一個怪問題。在 view 出現了 `LazyInitializationException - no session` 的錯誤。

在view層程式碼大概如下

```html=
<g:set var='user' value='${(User)session.getAttribute('user')}'/>
<g:set var='userPhoneNumber' value='${user.detail.phoneNumber}'/>
```

恩，和上次是一樣的問題，session中的物件是detached。那用attach 幫他重新建立proxy 

```html=
<g:set var='user' value='${(User)session.getAttribute('user')}'/>
<g:set var='userPhoneNumber' value='${user.attach().detail.phoneNumber}'/>
```

然而還是出現了一樣的錯誤，所以我就在 view 測試了一下

```htmlmixed=
<% 
    user.attach()
    System.out.println(user.isAttached())
%>
false
```
然而結果卻是False，照理來說這也是 view 層，為什麼無法開啟session 來做 Lazyloading

<p style="font-size:24px; font-weight:600">原因猜測</p>

因為當下這個 view 是用來做 layout 的，這是唯一和其他 view 不同的地方。Sitemesh 是 Grails 使用的 Template Engine，處理畫面的方式如下圖。

![sitemesh page render](https://i.imgur.com/zYRiNpU.png)

在request離開controller後，到真正送出頁面之前，畫面組成的順序會是 original view > compose with layout。

我花了滿多時間在找出官方說明，所以目前還不清楚是什麼機制造成(後面研究透徹後會補上)

但目前猜測出，**OSIV** 在original view 層還是有效的，但到了 layout 的時候，就沒有 session 提供給該 layout 使用。由於也沒有 session 可以用來與 PO attach，那當然無論怎麼樣都沒有辦法使用 Lazyloading 來取得深層的資料。

所以後來是利用 Interceptor 在 render view 之前，就先重新將在 httpSession 所要使用的物件重新 attach ，再放入 httpSession。

```groovy=
//Interceptor
boolean before() {
    User user = session['user']
    session.setAttribute('user', user.attach())
}
```

如此一來，即便在 Layout 沒有 session 可以讓 PO 重新和 session 建立關聯，在這次的 request view 完全送出之前，httpSession 中的 PO 仍然維持關聯。直到 request 結束後關閉 session。

## Another Exception - proxy with two open Sessions

>org.hibernate.HibernateException: illegally attempted to associate a proxy with two open Sessions

上述算是解決了 Layout 中沒辦法取得 session 重新 proxy 的問題。測試的時候也很完美，直到出現了上述的Exception。

這錯誤在一般的頁面 request 是不會有這問題發生。只有在該 html page 中有兩個以上的 ajax 同時送出請求，才會報錯。這算是稍微有點牽連到多執行緒與Hibernate Session 問題，但不在此篇討論範圍。

由於每個 request 都會有新的 hibernate session ，包括 ajax 的 request 。此時雖然是不同的 request ，但同樣的都會經過 Filter ，並透過剛才前一段的程式碼去重新 attach PO to session。這時候就會出問題了。

由於兩個 request 是不同的 session ，然而我們存在 httpSession 中的 PO 只有一個實體。這時候同時送出兩個 request 就會造成將一個實體綁定在兩個不同的 session 錯誤發生。

這時候只能讓兩個ajax錯開時間送 request ，必須要等到其中一個請求完成回來後，再發送下一個 ajax request。

## More.. DuplicateKeyException(spring) & NonUniqueObjectException(hibernate)

## 參考資料 references

[OpenSessionInViewFilter原理以及為什麼要用OpenSessionInViewFilter](https://www.itread01.com/content/1544980926.html)
[A Guide to Spring’s Open Session In View](https://www.baeldung.com/spring-open-session-in-view)
[Hibernate could not initialize proxy – no Session](https://www.baeldung.com/hibernate-initialize-proxy-exception)
[I don't like Grails/Hibernate part 3. DuplicateKeyException: Catch it if you can.
](http://rpeszek.blogspot.com/2014/08/i-dont-like-grailshibernate-part-3.html)