在「每類一表」或「單表」策略下,Hibernate 原生支援多態多對一關聯。但當關聯端使用延遲加載(代理)時,會遇到經典的 ClassCastException 陷阱。本文剖析問題根源,提供兩種解決方案。
ClassD 的屬性 a 可指向 ClassB 或 ClassC 實例。映射配置:
<many-to-one name="a" class="ClassA" column="A_ID" lazy="true" cascade="save-update"/>
ClassCastException當 lazy="true"(預設)時,Hibernate 返回 ClassA 的代理對象(CGLIB 動態子類),而非實際的 ClassB/ClassC 實例。直接強轉會失敗:
ClassA a = d.getA();
ClassB b = (ClassB) a; // 拋出 ClassCastException!代理不是 ClassB 實例
Session.load() 顯式加載目標子類前提:已知實際關聯的子類類型(如透過判別欄或業務邏輯)。
ClassA a = d.getA(); // 獲取代理,拿到 ID
ClassB b = (ClassB) session.load(ClassB.class, a.getId()); // 返回 ClassB 代理
System.out.println(b.getB1()); // 安全調用 ClassB 特有方法
原理:load() 不會立即查庫,返回指定類的代理,類型匹配,轉型安全。
使用 Criteria API 指定 FetchMode.EAGER,Hibernate 發起左外連接 SQL,直接實例化真實子類對象,而非代理:
ClassD d = (ClassD) session.createCriteria(ClassD.class)
.add(Expression.eq("id", id))
.setFetchMode("a", FetchMode.EAGER) // 關鍵:迫切抓取
.uniqueResult();
ClassA a = d.getA(); // 此時 a 是真實的 ClassB 或 ClassC 實例
if (a instanceof ClassB) System.out.println(((ClassB)a).getB1());
if (a instanceof ClassC) System.out.println(((ClassC)a).getC1());
生成 SQL 類似:SELECT ... FROM TABLE_D d LEFT JOIN TABLE_B b ON ... LEFT JOIN TABLE_C c ON ...
此策略無共享父表,TABLE_D 需雙欄:A_TYPE (判別) + A_ID (ID)。這正是 <any> 映射的適用場景(見下一篇)。
lazy="false" 全局關閉代理(效能代價大)。理解代理機制與多態關聯的交互,是避免 Hibernate 運行時異常、寫出高效數據訪問代碼的核心功力。