首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

Hibernate 延遲加載策略深度解析:類級別、關聯關係與 Open Session In View 模式全覆蓋

     編輯:Java 綜合教學

Hibernate 延遲加載策略深度解析

前言:為什麼需要延遲加載?

當 Hibernate 從資料庫加載實體(如 Dept 部門)時,若同時自動加載所有關聯實體(如 Emp 員工),但業務邏輯僅需訪問部門資訊,則會造成:

  1. SQL 語句過多:頻繁訪問資料庫,查詢效能低下
  2. 記憶體浪費:載入大量無用關聯物件佔用堆記憶體

這就是立即加載的弊端。延遲加載策略正是為解決此問題而生:按需加載,避免無效查詢,優化效能。


一、類級別加載策略

在 `.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. 延遲加載的邊界情況與陷阱

場景行為異常風險
查詢不存在的 IDload() 不拋異常,訪問屬性時拋 ObjectNotFoundException高
Session 關閉後訪問代理拋 LazyInitializationException極高
訪問 ID (getDeptNo())直接返回 OID,不觸發初始化無
Hibernate.initialize(proxy)在 Session 範圍內顯式初始化可控

4. 重要例外:get() 與 Query.list() 總是立即加載

無論類級別 `lazy` 設定為何,以下方法永遠立即查詢:

  • `session.get()`:不存在返回 null,不產生代理
  • `query.list()` / `iterate()`:直接查詢資料庫

二、關聯關係加載策略(一對多 / 多對一)

由 ``、`` 元素的 `lazy` 與 `fetch` 組合決定:

Fetch 策略對照

fetchlazy行為描述
select(預設)true多條 SQL,延遲加載
selectfalse多條 SQL,立即加載
selectextra極度延遲:size()/isEmpty() 不初始化,僅查 COUNT
join(忽略)左外連接,迫切立即加載
subselecttrue/false/extra子查詢模式,對應延遲/立即/極度延遲

一對多關聯實例(Dept → Emps)

<set name="emps" inverse="true" lazy="true">
    <key column="deptNo"/>
    <one-to-many class="Employee"/>
</set>

延遲加載驗證:

<set name="emps" lazy="extra">

特點:

  • `emps.size()` → 執行 `SELECT COUNT(*)`,不載入集合
  • `emps.iterator()` → 觸發完整集合初始化
  • 適合只需統計數量、不需遍歷明細的場景

多對一關聯策略(Employee → Dept)


三、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 異常的六大解決方案總結

  1. 改用 get():立即加載,無代理問題
  2. 顯式初始化:`Hibernate.initialize(obj)` 在 Session 關閉前調用
  3. 映射配置 lazy="false":關閉延遲加載(效能代價大)
  4. Biz 層預先訪問:在關閉 Session 前觸發所需屬性初始化
  5. 實體類加 final:禁止繼承,無法建立代理(副作用大,不推薦)
  6. Open Session In View:請求級 Session 管理(推薦 Web 專案)

最佳實踐建議

  • 預設使用延遲加載(`lazy="true"`),按需優化
  • 集合關聯優先考慮 `lazy="extra"` 避免誤觸發大集合載入
  • Web 專案採用 OSIV 模式或 Spring 的 `OpenSessionInViewFilter`
  • 避免在映射文件濫用 `fetch="join"` 導致 Cartesian Product 問題
  • 複雜查詢優先使用 HQL/JPQL `JOIN FETCH` 精確控制抓取策略

延遲加載是把雙面刃:用得好是效能利器,用不好是 Bug 溫床。理解其機制邊界,才能駕馭 Hibernate 的真正威力。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved
lazy 值代理方式初始化時機
proxy(預設)動態代理訪問任意非 ID 方法時
no-proxy字節碼儀器化調用 getter 方法時
false無代理立即加載(雙表查詢)