# :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)