// 領域模型:雙向一對多關聯
@Entity
public class NewsDir {
@Id @GeneratedValue(strategy = GenerationType.TABLE)
private Long id;
@Column(unique = true, nullable = false, length = 16)
private String sn;
private String title;
@OneToMany(mappedBy = "parent", cascade = CascadeType.REMOVE)
private List children = new ArrayList<>(); // 預設 LAZY
@ManyToOne
private NewsDir parent;
// getter/setter...
}
// MVC Action 方法
public Page doList(WebForm form, Module module) {
NewsDirQueryObject ndqo = new NewsDirQueryObject();
form.toPo(ndqo);
ndqo.setDel(true);
IPageList pageList = service.queryDirsByConditions(ndqo); // Service 層事務結束
CommUtilForTeaec.saveIPageList2WebForm(pageList, form);
form.addResult("dirPath", this.getDirPath(form));
return module.findPage("list");
}
// 模板文件 (Velocity/FreeMarker/JSP)
#foreach($info in ${dir.children}) // 此處觸發延遲載入
目錄名稱:${info.title}
#end
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.easyjf.cms.domain.NewsDir.children, no session or session was closed
at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationException(...)
at org.hibernate.collection.AbstractPersistentCollection.throwLazyInitializationExceptionIfNotConnected(...)
at org.hibernate.collection.AbstractPersistentCollection.readSize(...)
| 層級 | 事務狀態 | EntityManager / Session 狀態 |
|---|---|---|
| Controller (MVC) | 無事務 | 關閉 / 脫管 |
| Service (業務邏輯) | @Transactional (REQUIRED) | 開啟 → 提交/回滾 → 關閉 |
| View (模板渲染) | 無事務 | 已關閉 → 延遲載入失敗 |
核心衝突:Service 方法返回時事務結束,EntityManager 關閉,持久化上下文失效。View 層遍歷關聯集合時觸發延遲載入,但無可用 Session。
| 方案 | 原理 | 優點 | 缺點 | 適用場景 |
|---|---|---|---|---|
| Open Session In View (OSIV) | Filter/Interceptor 將 Session/EntityManager 生命週期延伸至整個 HTTP 請求 | 零業務代碼變更、透明解決延遲載入、成熟模式 | 長事務風險、資源佔用久、N+1 問題隱蔽、難以追蹤事務邊界 | 讀多寫少、CRUD 為主、團隊熟悉模式 |
| 事務前移至 Controller (本文方案) | 在 MVC 處理器 (Controller/Action) 開啟事務,Service 方法加入現有事務 | 事務邊界清晰、業務邏輯與事務一致、資源釋放及時 | 需調整事務配置、Controller 耦合事務、寫操作需謹慎 | 讀寫混合、需精確事務控制、團隊接受架構調整 |
| DTO/VO 組裝 (Eager Fetch) | Service 層主動載入所需關聯 (JOIN FETCH / EntityGraph),返回離線 DTO | 零延遲載入風險、明確資料邊界、效能可控 | 開發工作量大、樣板代碼多、靈活性低 | 高效能要求、固定視圖模型、API 介面 |
| @Transactional(readOnly=true) 在 Controller | Spring MVC @Controller 方法加註解開啟唯讀事務 | 配置簡單、語義清晰 | 僅支援 Spring MVC、寫操作需分離 | Spring MVC 專案、以查詢為主 |
<aop:config>
<aop:pointcut id="CmsManage" expression="execution(* com.easyjf.cms.service.*.*(..))"/>
<aop:advisor pointcut-ref="CmsManage" advice-ref="cmsManageAdvice"/>
</aop:config>
<tx:advice id="cmsManageAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="get*" propagation="SUPPORTS" read-only="true"/>
<tx:method name="query*" propagation="SUPPORTS" read-only="true"/>
</tx:attributes>
</tx:advice>
<!-- 定義 MVC 核心處理器切點 -->
<aop:pointcut id="easyjwebProcessor" expression="execution(* com.easyjf.web.RequestProcessor.process(..))"/>
<aop:advisor pointcut-ref="easyjwebProcessor" advice-ref="txEasyjwebProcessorAdvice"/>
<tx:advice id="txEasyjwebProcessorAdvice" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="process" propagation="REQUIRED" read-only="false"/>
</tx:attributes>
</tx:advice>
RequestProcessor.process() 是 EasyJWeb 所有請求的統一入口read-only="true",Hibernate 可優化快取/鎖策略
openEntityManagerInView
org.springframework.orm.jpa.support.OpenEntityManagerInViewFilter
entityManagerFactoryBeanName
entityManagerFactory
openEntityManagerInView
/*
或 Java Config:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public OpenEntityManagerInViewFilter openEntityManagerInViewFilter() {
OpenEntityManagerInViewFilter filter = new OpenEntityManagerInViewFilter();
filter.setEntityManagerFactoryBeanName("entityManagerFactory");
return filter;
}
}
@Controller
@Transactional(readOnly = true) // 類別級預設唯讀
public class NewsDirController {
@Autowired
private NewsDirService service;
@GetMapping("/list")
public String list(Model model, @RequestParam(defaultValue = "") String keyword) {
// 此方法在唯讀事務中執行,View 渲染前事務未提交
List dirs = service.queryByKeyword(keyword);
model.addAttribute("dirs", dirs);
return "newsdir/list";
}
@PostMapping("/save")
@Transactional(readOnly = false) // 方法級覆蓋為讀寫
public String save(@ModelAttribute NewsDir dir) {
service.save(dir);
return "redirect:/list";
}
}
read-only=true,修改請求 read-only=false,Hibernate 可啟用唯讀優化JPA 延遲載入失效本質是 Persistence Context 生命週期 < HTTP 請求生命週期。Spring 提供三大標準解法:OSIV Filter、Controller @Transactional、DTO 組裝。本文展示的「事務前移至 MVC 處理器」方案通過 AOP 切點攔截統一入口,以最小配置變更解決了 EasyJWeb 框架下的延遲載入問題。**核心洞見**:事務邊界應與「業務邏輯執行 + 視圖渲染」完整對齊,而非僅限於 Service 方法。架構演進方向:明確的資料需求聲明 (EntityGraph/DTO) 取代隱式的延遲載入,將資料邊界從運行時推遲到編譯時/設計時確定。