延遲加載和急加載
有些時候,必須決定應該講那些數據從數據庫中加載到內存中,當執行entityManager.find(Item.class,123)時,那些咋內存中是可用的并且被加載到持久化上下文中呢?,如果轉而使用EntityManager#getReference()又發生什么呢?
在域-模型映射中,要在關聯和集合上使用FetchType.LAZY和FetchType.EAGER選項來定義全局默認的抓取計劃,這個計劃是用于所有涉及持久化模型類的操作的默認設置.當通過標識符加載一個實體實例以及通過后續關聯導航實體圖并且遍歷持久化集合時.他總是處于活動狀態.
我們推薦策略是將延遲的默認抓取計劃用于所有實體和集合.如果使用FetchType.LAZY映射的所有的關聯和集合.那么Hibernate將只在你進行訪問的時候加載數據,當導航域模型實例的圖時,Hibernate會按需一塊一塊地加載數據.然后在必要時基于每種情況重寫此行為.
為實現延遲加載.Hibernate借助被稱為代理的運行時生成的實體占位符以及用于集合的智能包裝器.
選擇一個抓取策略
Hibernate會執行SQL SELECT語句講數據加載到內存中,如果加載一個實體實例,則會執行一個或者多個SELECT.這取決于涉及的表數量以及所應用的抓取策略.你的目標就是最小化SQL語句的數量.并且將會SQL語句,以便查詢盡可能提高效率.
每一個關聯和集合都應該按須被延遲加載.這一默認抓取計劃很可能造成過多的SQL語句,每個語句都僅加載一小部分數據.這將導致n+1次查詢問題.我們首先探討這個問題,使用急加載這一可選抓取計劃,將產生較少的SQL語句,因為每個SQL查詢都會將較大快的數據加載到內存中,然后你可能會看到笛卡爾積問題,因為SQL結果集變得過大.
需要在這個兩個極端之間找到平衡.用于應用程序中每個程序和用例的理想抓取策略.就像抓取計劃一樣.可以在映射中設置一個全局抓取策略.總是生效的默認設置,然后對于某特定程序.可以用JPQL.CriteriaQuery或SQL查詢重寫默認抓取策略.
n+1查詢問題
- 1 對多,在1 方,查找得到了n 個對象, 那么又需要將n 個對象關聯的集合取出,于是本來的一條sql查詢變成了n +1 條
- 多對1 ,在多方,查詢得到了m個對象,那么也會將m個對象對應的1 方的對象取出, 也變成了m+1
笛卡爾積問題
如果查看域和數據模型并且認為,每次我需要一個Item時.我還需要改Item的seller.那么可以使用FetchType.EAGER而非延遲抓取計劃來映射該關聯.你希望確保無論何時加載一個Item.seller都會被立即加載.您希望數據在分離Item和關閉持久化上下文時可用.
為了實現急抓取計劃.Hibernate使用了一個SQL JOIN操作在一個SELECT中加載Item和User實例.
select i.*,u.* from t_item i left outer join t_users u on u.id = i.seller_id where i.id=?
將使用默認JOIN策略的急抓取用于@ManyToOne和@OneToOne關聯沒什么問題.可以使用一個SQL查詢和多個JOIN急加載一個Item,其seller,該User的Address以及他們居住的City等,即便你使用FetchType.EAGER映射所有這些關聯.結果集也只有一行,現在,Hibernate必須在某個時刻停止繼續你的FetchType.EAGER計劃,所鏈接的表的數量取決于全局的Hibernate.max_fetch_depth配置屬性.默認情況下,不會設置任何限制,合理值很小,通常介意1到5之間.甚至可以通過該屬性設置為0來禁用@ManyToOne和@OneToOne關聯的JOIN抓取,如果Hibernate達到了該限制.那么它仍將根據您的抓取計劃急加載數據.但會使用額外的SELECT語句,
另一方面,使用JOINS的急加載集合會導致嚴重的性能問題.如果也為bids何images集合切換到FetchType.EAGER,就會碰到笛卡爾積問題.
這個問題會在用一個SQL查詢和一個JOIN操作急加載兩個集合時出現.看下面的例子.
@Table(name = "t_item")
public class Item {
@OneToMany(mappedBy = "Item", fetch = FetchType.EAGER)
private Set<Bid> bids = new HashSet<>();
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "IMAGE")
@Column(name = "FILENAME")
private Set<String> images = new HashSet<>();
}
這兩個集合是@OneToMany @ManyToMany還是@ ElementCollection并沒有什么關系.使用SQL JOIN操作符一次急抓取多個集合就是根本問題,無論集合內容是什么.如果加載一個Item,那么Hibernate就會執行有問題的SQL語句.
select i.*,b.*,img.* from Item i
left outer join Bid b on b.ITEM_ID = i.ID
left outer join Image img on img.ITEM_ID = i.ID
where i.ID = ?
Hibernate會服從你的急抓取計劃.并且可以訪問分離狀態中的bids和images集合.問題在于.使用產生一個乘機SQL JOIN,這些集合是如何別加載的.
該Item具有3個bids和3個images.乘積的大小取決于你正在檢索的大小.3*3=9,現在思考一個具有50個bids和5個images的Item的情況.你會看到具有250行的一個結果集.在使用JPQL或CriteriaQuery編寫你自己的查詢時你甚至會創建更大的SQL乘積.想象一個你在加載500個items并且使用多個JOIN急抓取幾十個bids和images時會發生什么么?
數據庫服務器上需要大量的處理時間和內存來創建這樣的結果.這些結果還必須跨網絡傳輸.如果寄希望于JDBC驅動法在傳輸是壓縮該數據.你可能對數據庫供應商的期望過高了.Hibernate會在將結果集封送到持久化實例和集合中時立即移除所有重復項.顯然.無法再SQL級別移除這些重復項.
接下來,我們要專注于此類優化以及如何找出并且實現最佳的抓取策略.我們還是從默認延遲抓取計劃開始并且首先嘗試解決n+1查詢問題
批量抓取數據
如果Hibernate僅按需抓取每個實體關聯和集合.那么可能就需要許多額外的SQL SELECT語句來完成某特定過程.像之前一樣,思考一個檢查每個Item的seller是否具有一個username的例子.使用延遲加載,就需要一個SELECT得到所有的Item實例以及更多的n個SELECT來初始化每個Item的seller代理.
Hibernate提供了幾個可以預抓取數據的算法.我們套探討的第一個算法是批量預抓取.它會如下所示的工作.如果Hibernate必須初始化一個User代理,那么就使用相同的SELECT初始化幾個User代理,換句話說,如果已經知道持久化上下文中有幾個Item實例并且他們都具有一個應用到其seller關聯的代理,那么久可以初始化幾個代理,而不是在于數據庫交互時只初始化一個代理
@Entity
@org.hibernate.annotations.BatchSize(size = 10)
@Table(name = "t_Users")
public class User implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
此設置會告知Hibernate,在必須加載一個User代理時它可以加載至10個,所有的代理都使用相同SELECT來加載.批量抓取通常被稱為忙猜優化,因為你不知道某特定持久化上下文中會有多少個未初始化的User代理,你不能確定10是否是一個理想值.它只是一個猜測.你清楚相較于n+1個SQL查詢.你現在回看到n+1/10個查詢.已經顯著減少了,合理值通常很小,因為你也不希望過多的數據加載到內存中,尤其是在您不確定是否需要他時,
注意.Hibernate在您遍歷items時執行SQL查詢.當首次調用item.getSeller().getUserName()時.Hibernate必須初始化第一個User代理.相較于僅從USERS表中加載單個行.Hibernate會檢索多個行,并且加載最多10個User實例.一旦訪問第十一個seller.就會在一個批次中加載另外10個.一次類推.
批量抓取也可用于集合:
@Entity
@Table(name = "t_item")
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "item")
@org.hibernate.annotations.BatchSize(size = 5)
private Set<Bid> bids = new HashSet<>();
批量抓取是簡單的.并且通常智能優化能夠顯著降低SQL語句的數量.否則初始化所有代理和集合就需要大量的SQL語句,盡管最終可能會預抓取你不需要的數據.并且消耗更多的內存,但數據庫交互的減少也會產生很大的差異,內存很便宜.但拓展數據庫服務器就并非如此了.
另一個并非盲猜的預抓取算法會使用子查詢在單個語句中初始化多個集合.
使用子查詢預抓取集合.
用于加載幾個Item實例的所有bids的更好的一個策略是使用一個子查詢進行預抓取.要啟用此優化.需要將一個Hibernate注解添加到你的集合映射:
@Entity
@Table(name = "t_item")
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "item")
@org.hibernate.annotations.Fetch(
FetchMode.SUBSELECT
)
private Set<Bid> bids = new HashSet<>();
Hibernate會記住用于加載item的原始查詢.然后他會在子查詢中嵌入這個初始查詢.以便為每個item檢索bids的集合.
如果在映射中堅持使用一個全局的延遲抓取計劃.那么批量和子查詢就會降低特定過程需要的查詢數量,以幫助緩解n+1查詢問題.如果相反,你的全局抓取計劃具有急加載關聯和集合,就必須避免笛卡爾積問題,例如.通過將一個join查詢分解成幾個SELECT來避免.
使用多個SELECT進行急抓取
當嘗試一個SQL查詢和多個JOIN抓取幾個集合時,就會碰到笛卡爾積問題.將像之前的闡述過的那樣,相較于一個JOIN操作,可以告知HIbernate用幾個額外的SELECT查詢急加載數據.并因而避免大的結果以及具有重復項的SQL乘積.
@Entity
@Table(name = "t_item")
public class Item {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy = "item", fetch = FetchType.EAGER)
@org.hibernate.annotations.Fetch(
FetchMode.SELECT
)
private Set<Bid> bids = new HashSet<>();
@ManyToOne(fetch = FetchType.EAGER)
@org.hibernate.annotations.Fetch(
FetchMode.SELECT
)
private User seller;
現在,當加載一個Item時,也必須加載seller和bids:
Item item = em.find(Item.class,ITEM_ID);
//select * from Item where id = ?
//select * from User where id = ?
//select * from Bid where ITEM_ID = ?
Hibernate會使用一個SELECT從ITEM表中加載一行,然后它會立即執行兩個SELECT;一個從USER表中加載一行(seller),另一個從BID表中加載幾行(bids).
額外的SELECT查詢不會被延遲執行:find()方法會生成幾個SQL查詢.可以看到Hibernate如何遵循急抓取計劃:所有數據在分離狀態下都是可用的.
動態急抓取
我們假設你必須檢查每個Item#seller的username,使用一個延遲全局抓取計劃,加載這個過程所需的數據并且在一個查詢中應用動態急抓取策略:
List<Item> items = em
.createQuery("select i from Item i join fetch i.seller");
//select i.*,u.* from Item i inner join User u on
//u.ID = i.SELLER_ID
//where i.ID= ?
這個JPQL查詢中的重要關鍵詞是join fetch,告知Hibernate使用一個SQL JOIN在相同查詢中檢索每個Item的Seller,也可以使用CriteriaQuery API而非JPQL字符串來表示相同的查詢.
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery criteria = cb.createQuery();
Root<Item> i = criteria.from(Item.class);
i.fetch("seller");
criteria.select(i)
List<Item> items = em.createQuery(criteria).getResultList();
動態急聯結抓取也使用與集合.此處要加載每個Item的所有bids:
List<Item> items = em.createQuery("select i from Item i left join fetch i.bids").getResultList();
//select i.*,b.* from Item i left outer join Bid b
//on b.ITEM_ID = i.ID
//where i.ID = ?
同樣也可以使用CriteriaQuery API
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery criteria = cb.createQuery();
Root<Item> i = criteria.from(Item.class);
i.fetch("bids",JoinType.LEFT);
criteria.select(i)
List<Item> items = em.createQuery(criteria).getResultList();