在前一篇文章中,我們探討了 Hibernate 的基本查詢操作。本篇將深入剖析 Query Cache(查詢快取)的運作原理、啟用方式,以及 list() 與 iterate() 兩種查詢方法在快取行為上的關鍵差異,幫助你在實際專案中做出最佳效能選擇。
我們先看一段基礎測試代碼,執行兩次相同的 HQL 查詢:
String hql = "from com.hb3.pack_22.model.User";
Query query = session.createQuery(hql);
List<?> users = query.list();
// ... 列印結果 ...
query = session.createQuery(hql);
users = query.list();
// ... 列印結果 ...
執行結果:
Hibernate: select user0_.id as id0_, user0_.name as name0_, user0_.room_id as room3_0_ from user user0_
chenyan
shenbin
Hibernate: select user0_.id as id0_, user0_.name as name0_, user0_.room_id as room3_0_ from user user0_
chenyan
shenbin
可以看到,每次呼叫 list() 都會發出實際的 SQL 查詢,資料庫被存取了兩次。這就是預設不開啟查詢快取的行為。
在 <session-factory> 區塊中加入:
<property name="hibernate.cache.use_query_cache">true</property>
建立 Query 物件後,必須呼叫 setCacheable(true):
Query query = session.createQuery(hql);
query.setCacheable(true); // 關鍵行
List<?> users = query.list();
同樣執行兩次查詢,第二次將不再發出 SQL,直接從快取返回結果:
Hibernate: select user0_.id as id0_, user0_.name as name0_, user0_.room_id as room3_0_ from user user0_
chenyan
shenbin
chenyan
shenbin
Hibernate 啟用查詢快取後,會以「查詢 SQL 字串 + 參數值」為鍵,儲存查詢結果的**識別值(通常是主鍵 ID)**。
INSERT/UPDATE/DELETE 異動。注意: Query Cache 快取的是「查詢結果的 ID 清單」,而非實體物件本身。實體物件仍依賴 Session 快取或二級快取。
list() vs iterate():快取行為大不同這是許多開發者容易誤解的關鍵點。
list():不讀取 Query Cache,但會寫入Query query = session.createQuery("from User");
List users = query.list(); // 第 1 次:查 DB,結果寫入 Query Cache
users = query.list(); // 第 2 次:**仍查 DB**,不讀 Query Cache!
SQL 輸出:
Hibernate: select ... from user
Hibernate: select ... from user <-- 重複查詢!
list() 適合「一次性讀取所有資料」的場景,不依賴查詢快取讀取,但會更新快取內容。
iterate():讀取 Query Cache,逐筆載入實體Query query = session.createQuery("from User");
Iterator users = query.iterate(); // 第 1 次:查 DB 取 ID,再逐筆載入實體
users = query.iterate(); // 第 2 次:讀 Query Cache 取 ID,再逐筆載入實體
SQL 輸出:
Hibernate: select user0_.id as col_0_0_ from user user0_ <-- 只撈 ID
Hibernate: select ... from user ... where user0_.id=? <-- 逐筆撈實體
Hibernate: select ... from user ... where user0_.id=?
Hibernate: select user0_.id as col_0_0_ from user user0_ <-- 第 2 次只撈 ID
chenyan
shenbin <-- 實體從 Session Cache 取得,無額外 SQL
iterate() 會將查詢到的 ID 存入 Query Cache,下次相同查詢直接讀取 ID 清單,再利用 Session 快取(一級快取)逐筆解析實體。
我們設計一個場景:查詢 User 並存取其關聯的 Room(多對一),觀察 SQL 數量。
iterate() 兩次Iterator<?> it = query.iterate();
while(it.hasNext()) { User u = (User)it.next(); System.out.println(u.getName()); }
System.out.println("=== 分隔 ===");
it = query.iterate();
while(it.hasNext()) { User u = (User)it.next(); System.out.println(u.getName()); }
結果: 第一次查詢發出 1 條 ID 查詢 + N 條實體查詢(N=筆數);第二次只有 1 條 ID 查詢,實體從 Session Cache 取得。**但第一次的 N+1 查詢極其低效。**
list(),第二次 iterate()List<?> list = query.list();
Iterator<?> it = list.iterator();
while(it.hasNext()) { User u = (User)it.next(); System.out.println(u.getRoom().getAddress()); }
System.out.println("=== 分隔 ===");
it = query.iterate();
while(it.hasNext()) { User u = (User)it.next(); System.out.println(u.getName()); }
結果: 第一次 list() 以 **單條 JOIN SQL** 取回所有 User 及 Room 資料;第二次 iterate() 讀 Query Cache 取 ID,實體從 Session Cache 直接拿,**零資料庫存取**。
結論: 首次查詢用list()(配合 fetch join)效能最佳;後續相同查詢用iterate()利用快取極致提升效能。
iterate() 會將實體載入 Session 快取,大量資料時記憶體壓力大。必要時呼叫 session.evict(entity) 或 session.clear() 釋放。
setCacheable(true)。list() 不讀 Query Cache,適合首次大量載入;iterate() 讀 Query Cache,適合重複查詢。list()(含 fetch join)→ 後續 iterate()**。善用這機制,能大幅減少資料庫負載,提升系統回應速度。