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