Hibernate 延遲加載策略深度解析
前言:為什麼需要延遲加載?
當 Hibernate 從資料庫加載實體(如 Dept 部門)時,若同時自動加載所有關聯實體(如 Emp 員工),但業務邏輯僅需訪問部門資訊,則會造成:
- SQL 語句過多:頻繁訪問資料庫,查詢效能低下
- 記憶體浪費:載入大量無用關聯物件佔用堆記憶體
這就是立即加載的弊端。延遲加載策略正是為解決此問題而生:按需加載,避免無效查詢,優化效能。
一、類級別加載策略
在 `.hbm.xml` 映射文件的 `` 元素配置 `lazy` 屬性:
lazy="true"(預設):延遲加載,返回代理物件
lazy="false":立即加載,直接查詢資料庫
1. 立即加載驗證
<class name="Dept" table="dept" lazy="false"> ... </class>
測試代碼:
Session session = HibernateUtil.currentSession();
session.beginTransaction();
Dept dept = (Dept) session.load(Dept.class, 1);
System.out.println("部門名稱: " + dept.getdName());
session.getTransaction().commit();
結果:執行 `load()` 即發送 SQL 查詢,非代理物件。
2. 延遲加載(預設行為)
<class name="Dept" table="dept" lazy="true">
特點:
- `session.load()` 返回代理物件,僅含 OID,不發 SQL
- 首次訪問非 ID 屬性時初始化,發送 SQL
- 適用場景:僅需關聯引用(如設定外鍵)而非訪問屬性
3. 延遲加載的邊界情況與陷阱
| 場景 | 行為 | 異常風險 |
| 查詢不存在的 ID | load() 不拋異常,訪問屬性時拋 ObjectNotFoundException | 高 |
| Session 關閉後訪問代理 | 拋 LazyInitializationException | 極高 |
| 訪問 ID (getDeptNo()) | 直接返回 OID,不觸發初始化 | 無 |
| Hibernate.initialize(proxy) | 在 Session 範圍內顯式初始化 | 可控 |
4. 重要例外:get() 與 Query.list() 總是立即加載
無論類級別 `lazy` 設定為何,以下方法永遠立即查詢:
- `session.get()`:不存在返回 null,不產生代理
- `query.list()` / `iterate()`:直接查詢資料庫
二、關聯關係加載策略(一對多 / 多對一)
由 ``、`` 元素的 `lazy` 與 `fetch` 組合決定:
Fetch 策略對照
| fetch | lazy | 行為描述 |
| select(預設) | true | 多條 SQL,延遲加載 |
| select | false | 多條 SQL,立即加載 |
| select | extra | 極度延遲:size()/isEmpty() 不初始化,僅查 COUNT |
| join | (忽略) | 左外連接,迫切立即加載 |
| subselect | true/false/extra | 子查詢模式,對應延遲/立即/極度延遲 |
一對多關聯實例(Dept → Emps)
<set name="emps" inverse="true" lazy="true">
<key column="deptNo"/>
<one-to-many class="Employee"/>
</set>
延遲加載驗證:
特點:
- `emps.size()` → 執行 `SELECT COUNT(*)`,不載入集合
- `emps.iterator()` → 觸發完整集合初始化
- 適合只需統計數量、不需遍歷明細的場景
多對一關聯策略(Employee → Dept)
| lazy 值 | 代理方式 | 初始化時機 |
| proxy(預設) | 動態代理 | 訪問任意非 ID 方法時 |
| no-proxy | 字節碼儀器化 | 調用 getter 方法時 |
| false | 無代理 | 立即加載(雙表查詢) |
三、Open Session In View (OSIV) 模式
問題背景
Service 層返回啟用延遲加載的實體給 Web 層(JSP/Controller),但 Service 結束時 Session 已關閉。Web 層嘗試導航關聯屬性時拋出:
org.hibernate.LazyInitializationException:
could not initialize proxy - no Session
OSIV 解決方案:請求範圍內保持 Session 開啟
核心思路:使用 Filter 在請求進入時開啟 Session、事務提交,響應完成後關閉 Session。
關鍵組件實現
1. HibernateUtil(ThreadLocal 管理 Session)
public class HibernateUtil {
private static final ThreadLocal sessionTL = new ThreadLocal<>();
private static final SessionFactory sessionFactory;
static {
Configuration cfg = new Configuration().configure();
sessionFactory = cfg.buildSessionFactory();
}
public static Session currentSession() {
Session s = sessionTL.get();
if (s == null) {
s = sessionFactory.openSession();
sessionTL.set(s);
}
return s;
}
public static void closeSession() {
Session s = sessionTL.get();
sessionTL.set(null);
if (s != null) s.close();
}
}
2. DAO / Biz 層正常使用 load()
public Object get(Class clazz, Serializable id) {
return HibernateUtil.currentSession().load(clazz, id);
}
3. OSIV Filter
public class OpenSessionInViewFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
Session session = null;
Transaction tx = null;
try {
session = HibernateUtil.currentSession();
tx = session.beginTransaction();
chain.doFilter(req, resp); // 執行 Controller -> JSP
tx.commit();
} catch (HibernateException e) {
if (tx != null) tx.rollback();
throw new ServletException(e);
} finally {
HibernateUtil.closeSession();
}
}
}
4. web.xml 配置
<filter>
<filter-name>openSessionInView</filter-name>
<filter-class>cn.happy.filter.OpenSessionInViewFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>openSessionInView</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
JSP 中直接導航關聯屬性
<%
HibernateBiz biz = new HibernateBiz();
Emp emp = (Emp) biz.get(Emp.class, 1);
%>
<%= emp.getEmpName() %>
<%= emp.getDept().getDeptName() %> <!-- 延遲加載在此觸發,Session 仍開啟 -->
四、No Session 異常的六大解決方案總結
- 改用 get():立即加載,無代理問題
- 顯式初始化:`Hibernate.initialize(obj)` 在 Session 關閉前調用
- 映射配置 lazy="false":關閉延遲加載(效能代價大)
- Biz 層預先訪問:在關閉 Session 前觸發所需屬性初始化
- 實體類加 final:禁止繼承,無法建立代理(副作用大,不推薦)
- Open Session In View:請求級 Session 管理(推薦 Web 專案)
最佳實踐建議
- 預設使用延遲加載(`lazy="true"`),按需優化
- 集合關聯優先考慮 `lazy="extra"` 避免誤觸發大集合載入
- Web 專案採用 OSIV 模式或 Spring 的 `OpenSessionInViewFilter`
- 避免在映射文件濫用 `fetch="join"` 導致 Cartesian Product 問題
- 複雜查詢優先使用 HQL/JPQL `JOIN FETCH` 精確控制抓取策略
延遲加載是把雙面刃:用得好是效能利器,用不好是 Bug 溫床。理解其機制邊界,才能駕馭 Hibernate 的真正威力。
- 上一篇文章: Java 操作 CSV 文件實戰:基於 JavaCsv 庫的讀寫與轉換範例
- 下一篇文章: Java 檢測 URL 連通性實戰:HttpURLConnection 實現與超時控制
Copyright ©
程式碼之家 All Rights Reserved