# :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)  簡單來說,每個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)**  ::: ## 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,處理畫面的方式如下圖。  在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)
×
Sign in
Email
Password
Forgot password
or
Sign in via Google
Sign in via Facebook
Sign in via X(Twitter)
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
Continue with a different method
New to HackMD?
Sign up
By signing in, you agree to our
terms of service
.