Hibernate 的檢索策略本質上是為了解決效能問題而設計的核心機制。透過合理配置檢索策略,開發者可以在「立即載入」與「延遲載入」之間找到平衡點,避免 N+1 查詢問題,同時減少不必要的資料庫往返。策略主要分為兩大層級:類級別檢索策略(Class-level)與關聯級別檢索策略(Association-level),兩者各自控制不同範圍的物件載入時機與方式。
<class> 元素的 lazy 屬性類級別策略透過 <class> 節點的 lazy 屬性控制,預設為 true(延遲檢索)。此策略僅對 Session.load() 方法生效,Session.get() 會直接執行查詢並回傳完整物件。
session.load(Entity.class, id) 時,Hibernate 回傳一個代理物件(Proxy),此時僅初始化 OID(識別碼)。getName())時,Hibernate 才發送 SQL 查詢,初始化代理物件的完整狀態。session.load() 會立即執行 SELECT 語句,直接回傳完整初始化的實體物件,而非代理。// lazy="true" (預設)
@Test
public void testLazyTrue() {
Customer customer = (Customer) session.load(Customer.class, 5);
System.out.println(customer.getClass()); // 輸出代理類別名稱,如 Customer_$$_javassist_0
System.out.println(customer.getCustomerId()); // 僅讀取 OID,不觸發 SQL
System.out.println("--------------");
System.out.println(customer.getCustomerName()); // 首次存取非 OID 屬性,觸發 SELECT
}
/* 執行結果:
class com.nucsoft.strategy.many2one.Customer_$$_javassist_0
5
--------------
Hibernate: select customer0_.customer_id as customer1_2_0_, customer0_.customer_name as customer2_2_0_ from hibernate.customer customer0_ where customer0_.customer_id=?
bb
*/
// lazy="false"
@Test
public void testLazyFalse() {
session.load(Customer.class, 5); // 立即執行查詢
}
/* 執行結果:
Hibernate: select customer0_.customer_id as customer1_2_0_, customer0_.customer_name as customer2_2_0_ from hibernate.customer customer0_ where customer0_.customer_id=?
*/
<set> 元素的三大屬性關聯級別策略針對集合映射(<set>、<list> 等)與多對一、多對多關聯,核心屬性為 lazy、fetch、batch-size。
| 取值 | 行為描述 | 適用場景 |
|---|---|---|
true(預設) | 延遲檢索。存取集合屬性、呼叫 iterator()、size()、isEmpty()、contains() 或 Hibernate.initialize() 時才初始化。 | 大多數場景,避免載入不需要的集合資料。 |
false | 立即檢索。載入主物件時同步載入關聯集合(通常透過外連結或額外 SELECT)。 | 確定會使用集合資料,且集合較小。 |
extra | 增強型延遲檢索。size()、isEmpty()、contains() 僅執行 COUNT 查詢,不載入實體;iterator() 才真正初始化集合。 | 超大集合,僅需統計筆數或檢查存在性時極具優勢。 |
@Test
public void testSetLazyExtra() {
Category category = (Category) session.get(Category.class, 5);
System.out.println(category.getItems().size()); // 呼叫 size()
}
/* 執行 SQL:
Hibernate: select category0_.category_id as category1_1_0_, category0_.category_name as category2_1_0_ from hibernate.category category0_ where category0_.category_id=?
Hibernate: select count(item_id) from hibernate.categories_items where category_id =? -- 僅統計筆數
2
*/
| 取值 | 查詢方式 | 是否忽略 lazy | 備註 |
|---|---|---|---|
select(預設) | 獨立 SELECT 查詢,每個集合一條 SQL。 | 否 | 配合 batch-size 可優化為批次查詢。 |
subselect | 子查詢方式,一次查詢所有父實體的關聯集合(IN 子查詢)。 | 否 | 忽略 batch-size;適合先查詢父實體列表,再批次載入子集合的場景。 |
join | 迫切左外連結(Left Outer Join),單條 SQL 同時載入主物件與集合。 | 是(強制立即載入) | Session.get()/load() 生效;HQL/JPQL list() 會忽略此設定,需在查詢中显式 JOIN FETCH。 |
@Test
public void testFetchSelect() {
List<Category> categories = session.createQuery("from Category").list();
for (Category c : categories) {
System.out.println(c.getItems().size()); // 每次迴圈觸發一條 SELECT
}
}
/* SQL 輸出(假設 4 筆 Category):
Hibernate: select category0_.category_id ... from hibernate.category category0_
Hibernate: select items0_.category_id ... where items0_.category_id=? -- 第 1 筆
Hibernate: select items0_.category_id ... where items0_.category_id=? -- 第 2 筆
Hibernate: select items0_.category_id ... where items0_.category_id=? -- 第 3 筆
Hibernate: select items0_.category_id ... where items0_.category_id=? -- 第 4 筆
*/
/* SQL 輸出:
Hibernate: select category0_.category_id ... from hibernate.category category0_
Hibernate: select items0_.category_id ... from hibernate.categories_items items0_
inner join hibernate.item item1_ on items0_.item_id=item1_.item_id
where items0_.category_id in (select category0_.category_id from hibernate.category category0_)
-- 單條子查詢載入所有分類的項目
*/
@Test
public void testFetchJoin() {
session.get(Customer.class, 5); // 單筆載入時生效
}
/* SQL 輸出:
Hibernate: select customer0_.customer_id ..., orders1_.order_id ...
from hibernate.customer customer0_
left outer join hibernate.order orders1_ on customer0_.customer_id=orders1_.customer_id
where customer0_.customer_id=?
*/
當 fetch="select" 且 lazy="true" 時,batch-size 可將多筆集合初始化合併為單條 IN 查詢,有效緩解 N+1 問題。
// 未設定 batch-size(預設) /* SQL: Hibernate: select category0_... from hibernate.category category0_ Hibernate: select items0_... where items0_.category_id=? -- id=1 Hibernate: select items0_... where items0_.category_id=? -- id=2 */ // 設定 batch-size="2" 於 <set> 元素 /* SQL: Hibernate: select category0_... from hibernate.category category0_ Hibernate: select items0_... where items0_.category_id in (?, ?) -- 批次查詢 id=1,2 */
<many-to-one> 關聯的 lazy 與 fetch 屬性| 屬性 | 取值 | 說明 |
|---|---|---|
lazy | proxy(預設) | 延遲檢索,回傳目標實體的代理物件。 |
false | 立即檢索,直接載入關聯實體。 | |
fetch | select(預設) | 獨立 SELECT 查詢關聯實體。 |
join | 迫切左外連結載入關聯實體(忽略 lazy)。HQL 查詢會忽略此設定,需改用 JOIN FETCH 或在「1 的一端」<class> 設定 batch-size 批次載入代理。 |
JOIN FETCH 或 Criteria setFetchMode(FetchMode.JOIN) 精確控制,而非依賴映射檔的 fetch="join"(會影響所有查詢)。size()/contains() 僅執行 COUNT,效能提升顯著。掌握 Hibernate 檢索策略的核心在於「按業務場景精準控制 SQL 產生時機與形式」,而非單純追求「全延遲」或「全立即」。合理組合類級別與關聯級別策略,搭配 batch-size、subselect、extra 等進階選項,才能在開發效率與執行效能之間達到最佳平衡。