Hibernate的抓取策略及優化

Defining the global fetch plan

Retrieving persistent objects from the database is one of the most interesting parts of working with Hibernate.

The object-retrieval options

Hibernate提供了以下方式,從數據庫中獲取對象:

  • 如果persistence context是開啟的,訪問實體對象的get方法來訪問關聯實體對象,Hibernate會自動從數據庫中加載關聯實體對象
  • 通過ID主鍵查詢
  • HQL/JPA QL
  • Criteria接口,QBC/QBE
  • SQL查詢,包括調用存儲過程
HQL & JPA QL

JPA QL是HQL的一個subset,所以JPA QL總是一個有效的HQL。
HQL通常用于object retrieval,不太適合更新、插入、刪除,但是如果數據量比較多(mass data operations),可以利用HQL/JPA QL更新、插入、刪除,即direct bulk operation,第12章有講到。

通常利用HQL來查詢某實體對象,如下:

Session session = getSessionFactory().getCurrentSession();
Query query = session.createQuery("from User as u where u.firstname = :fname");
query.setString("fname", "John");
List list = query.list();

HQL is powerful,HQL支持以下功能(在14,15章將深入這些功能):

  • 查詢條件支持使用關聯對象的屬性
  • 只查詢entity的某些屬性,不會加載entity全部屬性到persistence context. 這叫report query或是projection
  • 查詢結果排序
  • 分頁
  • 使用group by, having聚合(aggregation),以及聚合函數,比如:sum, min, max/min.
  • outer join,當在一行要查詢多個對象時
  • 可以調用標準或用戶定義的SQL function.
  • 子查詢(subqueries/nested queries)
Criteria

QBC-query by criteria,通過操作criteria對象來創建query,同HQL相比,可讀性較差。

Session session = getSessionFactory().getCurrentSession();
Criteria criteria = session.createCriteria(User.class);
criteria.add(Restrictions.like("firstname", "John"));
List list = criteria.list();

Restrictions類提供了許多靜態方法來生成Criterion對象。Criteria是Hibernate API,JPA標準中沒有。

Querying by example

QBE-query by example,查詢符合指定對象屬性值的結果。

Session session = getSessionFactory().getCurrentSession();
Criteria criteria = session.createCriteria(User.class);

User user = new User();
user.setName("Jonh");

criteria.add(Example.create(user));
criteria.add(Restrictions.isNotNull("email"));
List list = criteria.list();

QBE適合在搜索時使用,比如頁面有一系列的搜索條件,用戶可以隨意指定某些條件來查詢,可以將這些條件自動封裝成一個對象,再利用QBE來查詢,省去自己組裝查詢語句。

第15章將具體討論QBC,QBE

The lazy default fetch plan

延遲加載,Hibernate對所有的實體和集合默認使用延遲加載策略(原文: Hibernate defaults to a lazy fetching strategy for all entities and collections.)。

User user = (User) session.load(User.class, 1L);

查詢User對象,通過load()方法查詢指定ID的對象,此時persistence context中,有一個user對象,并且是persistent state。

其實這個user對象是Hibernate創建的一個proxy(代理),這個代理對象的主鍵值是1,執行load()方法后,不會執行SQL去查詢指定對象。

Understanding proxies

當Hibernate返回entity instance時,會檢查是否能返回一代理,從而避免去查詢數據庫。當代理對象第一次被訪問時,Hibernate就會去數據庫查詢真正的對象。

User user = (User) session.load(User.class, 1L);
user.getId();
user.getName(); // Initialize the proxy

代碼執行到第三行時才去查詢數據庫。

當你只是需要一個對象來創建引用關系時,代理非常有用。如:

Item item = (Item) session.load(Item.class, new Long(123));
User user = (User) session.load(User.class, new Long(1234));

Bid newBid = new Bid("99.99");
newBid.setItem(item);
newBid.setBidder(user);

session.save(newBid);

BID表中有兩個外鍵列,分別指向ITEM表和USER表,所以只需要ITEM和USER的主鍵值,而代理對象中包含有主鍵值,所以不需要執行SELECT查詢數據庫。

如是是調用get()方法,則總是試圖去數據庫中查詢;如果當前persistence context和second-level cache中不存在指定對象就查詢數據庫,如果數據庫中沒有返回null

Hibernate返回的代理對象,其類型是實體類的子類,如果要獲得真正的實體類型:

User user = (User) session.load(User.class, new Long(1234));
// 返回真正的實體類Class
Class userClass = HibernateProxyHelper.getClassWithoutInitializingProxy(user);

JPA中對應的方法:

// find()相當于Hibernate的get()
Item item = entityManager.find(Item.class, new Long(123));

// getReference()相當于Hibernate的load()
Item itemRef = entityManager.getReference(Item.class, new Long(1234));

當獲取一個Item實例,不管是通過get()方法,還是load()方法然后再訪問非ID的屬性強迫初始化。此時實例的狀態如下圖所示:


可以看到所有的關聯對象(association)屬性都是代理(proxy),所有的集合屬性也沒有真正初始化,用術語collection wrapper表示。默認情況下,只有value-typed屬性和component會被立即初始化。
當訪問代理對象的非ID屬性時,代理被真正初始化;當迭代集合或調用集合的方法,如size(), contains()時,集合被真正初始化。

針對數據比較多的集合,Hibernate進一步提供了Extra lazy,即使在調用集合方法size(), contains(), isEmpty()時,也不會去真正查詢集合中的對象。
如果集合是Map或List,containsKey(), get()方法會直接查詢數據庫。

@org.hibernate.annotations.LazyCollection(org.hibernate.annotations.LazyCollectionOption.EXTRA)
private Set<Bid> bids = new HashSet<Bid>();

Disabling proxy generation

不使用代理,JPA規范中沒有代理(至少JPA 1.0是這樣),但如果使用Hibernate做為JPA實現,默認是啟用代理的,可以為某個實體類設置關閉代理:

@Entity
@Table(name = "USERS")
@org.hibernate.annotations.Proxy(lazy = false)
public class User implements Serializable, Comparable {
    // ...
}

如果這樣全局禁用User類的代理,則load()方法在加載User對象時就不會再返回代理;同時查詢關聯User的其他類對象時,也不會生成User代理對象,而是直接查詢數據庫。

// lazy = false,代理被禁用,不再生成代理對象,直接查詢數據庫
User user = (User) session.load(User.class, new Long(123));
User user = em.getReference(User.class, new Long(123));

// Item對象關聯User對象,默認為Item中User屬性生成代理
// 但當User類禁用代理后,不會再為User屬性生成代理,而要查詢數據庫初始化User對象
Item item = (Item) session.get(Item.class, new Long(123));
Item item = em.find(Item.class, new Long(123));

這種全局禁用代理,太粗粒度(too coarse-grained),通過只為特定的關聯實體(associated entity)或集合禁用代理,達到細粒度控制(fine-grained)。

Eager loading of associations and collections

Hibernate默認對關聯實體和集合使用代理,這就導致如果確實需要某個關聯實體或集合,還要再查詢一次數據庫來加載這些數據。
還有,當Item對象從persitent state變成detached state后,如果你還想訪問Item的關聯對象seller,那在加載Item對象時可以對seller不使用代理,從而在detached state還能訪問真正的seller對象而不是一個代理。

為關聯實體和集合禁用延遲加載:

@Entity
@Table(name = "ITEM")
public class Item implements Serializable, Comparable, Auditable {

    @ManyToOne(fetch = FetchType.EAGER)
    private User seller;

    @ManyToOne(fetch = FetchType.LAZY)
    private User approvedBy;

    @OneToMany(fetch = FetchType.EAGER)
    private List<Bid> bids = new ArrayList<Bid>();

}
Item item = (Item) session.get(Item.class, new Long(123));

此時訪問通過get()獲取或者強致初始化一個代理Item對象時,seller關聯對象和bids集合都被加載到persistence context中,而approveBy關聯對象仍然是代理對象。如果此時關閉persistence context,item變成detached state,此時可以訪問seller和bids,但是如果訪問approvedBy關聯對象,就會拋出LazyInitializationException,因為你沒有在persistence context關閉之前初始化approvedBy。

JPA同Hibernate的fetch plan不同,盡管Hibernate中所有的關聯實體都默認是延遲加載的,但@ManyToOne@OneToOne關聯映射默認是FetchType.EAGER,這是JPA 1.0規范要求的,因為有些JPA實現根本沒有lazy loading,所以建議為to-one關聯映射都添加上FetchType.LAZY,只有需要禁用延遲加載時再設置為FetchType.EAGER

@Entity
@Table(name = "ITEM")
public class Item implements Serializable, Comparable, Auditable {

    @ManyToOne(fetch = FetchType.EAGER)
    private User seller;

    @ManyToOne(fetch = FetchType.LAZY)
    private User approvedBy;
}

Lazy loading with interception(可忽略,不重要)

延遲加載除了可以使用代理,還可以使用interception來實現。使用代理,實體類需要滿足兩個條件:package or public的無參構造器,不能有final的方法和類。因為Hibernate的代理是通過創建實體類的子類來實現的。

代理方式的兩個小問題:

  1. 不能使用instanceof和類型轉換(typecast),因為代理是在運行時創建的一個實體類子類。(第7章-Polymorphic many-to-one associations有變通方法來解決此問題)
  2. 代理只延遲加載entity associations and collections,不能用于value-typed properties or components。 不過通常不需要在這個級別做延遲加載。

interception可以解決這兩個問題,但是這些問題不重要,所以不在此討論該功能了,如果需要請Google。


Selecting a fetch strategy

fetching strategy,為了減少查詢SQL的數量以及精簡SQL,從而讓查詢變的更加高效,我們需要為collection or association選擇合適的抓取策略。

Hibernate默認采用延遲加載(這里假定在使用JPA時,所有的to-one關聯都設置了FetchType.LAZY),所以當加載某個實體對象時,只會加載實體類對應表中的一行數據,所有關聯的表都不會查詢。當訪問proxied association or uninitialized collection時,Hibernate會立即執行一條SELECT語句來查詢所需要對象(可以稱為 fetch on demand)。

Prefetching data in batches

fetch on demand的不好之處,舉個例子:

List allItems = session.createQuery("from Item").list();
processSeller( (Item)allItems.get(0) ); // 此方法需要item的seller關聯對象
processSeller( (Item)allItems.get(1) );
processSeller( (Item)allItems.get(2) );

如果是默認的fetch on demand,就會導致大量的SELECT查詢:

select items...
select u.* from USERS u where u.USER_ID = ?
select u.* from USERS u where u.USER_ID = ?
select u.* from USERS u where u.USER_ID = ?

為了避免因為fetch on demand而可能導致的大量SELECT查詢,就需要更加有效的fetching strategy,首先就是batch fetching or prefetch in batch

批量獲取,上面例子中,當有一個seller association代理對象需要被初始化時,可以批量查詢USERS表,初始化當前persistence context中所有未初始化的seller association,但是一條SELECT語句最多可查詢幾個seller association是需要你來設定的。

@Entity
@Table(name = "USERS")
@org.hibernate.annotations.BatchSize(size = 10)
public class User { ... }
@Entity
public class Item {
    @OneToMany
    @org.hibernate.annotations.BatchSize(size = 10)
    private Set<Bid> bids = new HashSet<Bid>();
}

如上,現在設置User的@BatchSize為10,代表一條SELECT語句最多查詢10個User對象。
之前代碼生成的查詢就變成:

select items...
select u.* from USERS u where u.USER_ID in (?, ?, ?)

原來的三條SELECT,現在由一條SELECT完成了。

@BatchSize設置為10,如果第一次查詢的Item對象大于10個,當訪問第一個未初始化的seller association時,Hibernate會執行一條SELECT,查詢10個User對象來初始化10個seller association,如果又訪問到一個未初始化的seller association,Hibernate會再執行一個SELECT,再查詢10個User對象,直到persistence context中沒有未初始化的seller association或都程序不再訪問未初始化的seller association。

batch fetchingblind-guess optimization,因為你不知道真正需要加載多少個未初始化的association or collection。雖然這種方法可以減少SELECT查詢,但是也可能造成:加載了本就不需要加載的數據到內存中

Prefetching collections with subselects

為集合設置subselect fetching抓取策略,subselect只支持collection(至少在Hiberante 3.x是這樣的)。

@OneToMany
@org.hibernate.annotations.Fetch(
org.hibernate.annotations.FetchMode.SUBSELECT
)
private Set<Bid> bids = new HashSet<Bid>();
List allItems = session.createQuery("from Item").list();
processBids( (Item)allItems.get(0) ); //需要用到bids集合
processBids( (Item)allItems.get(1) );
processBids( (Item)allItems.get(2) );
select i.* from ITEM i
select b.* from BID b where b.ITEM_ID in (select i.ITEM_ID from ITEM i)

首先會查出所有Item對象,然后當訪問一個未初始化的collection時,Hibernate就會執行第二條SQL,為所有的Item對象初始化所有的bids collection。注意子查詢語句基本就是第一個查詢語句,Hibernate會重用第一個查詢(簡單修改,只查詢ID)。所以subselect fetching只對查詢Item訪問Item對象的bids屬性是在一個persistence context中時才會有效。

Also note that the original query that is rerun as a subselect is only remembered by Hibernate for a particular Session. If you detach an Item instance without initializing the collection of bids, and then reattach it and start iterating through the collection, no prefetching of other collections occurs.

Eager fetching with joins

Hibernate默認是延遲加載--fetch on demand,認為你可能不需要association or collection,但如果你確實需要association or collection呢,就需要eager fetching,通過join來關聯多張表,同時返回多張表的數據,從而在一條SQL中返回所有需要的數據。

<class name="Item" table="ITEM">
    <many-to-one name="seller"
                class="User"
                column="SELLER_ID"
                update="false"
                fetch="join"/>
</class>
@Entity
public class Item {
    // fetch = FetchType.EAGER等價與XML映射中的fetch="join"
    @ManyToOne(fetch = FetchType.EAGER)
    private User seller;
}

此時Hibernate在加載Item時,就會通過JOIN在一條SQL中把seller對象也加載出來。至于是outer join還是inner join這取決于你是否啟用了<many-to-one not-null="true"/>,如果確保實體對象肯定有指定的關聯對象,則使用inner join,否則默認是outer join。

Item item = (Item) session.get(Item.class, new Long(123));
select i.*, u.*
from ITEM i
    left outer join USERS u on i.SELLER_ID = u.USER_ID
where i.ITEM_ID = ?

如果設置lazy="false",也會禁用延遲加載,但不是通過JOIN在一條SQL中查詢關聯對象,而是查詢完實體對象后,緊接著再發送一條SELECT查詢關聯實體對象。

<class name="Item" table="ITEM">
    <many-to-one name="seller"
                class="User"
                column="SELLER_ID"
                update="false"
                lazy="false"/>
</class>

注解的方式如下:

@Entity
public class Item {
    @ManyToOne(fetch = FetchType.EAGER)
    @org.hibernate.annotations.Fetch(
        org.hibernate.annotations.FetchMode.SELECT
    )
    private User seller;
}

為collection設置eager join fetching strategy.

<class name="Item" table="ITEM">
    <set name="bids" inverse="true" fetch="join">
            <key column="ITEM_ID"/>
            <one-to-many class="Bid"/>
    </set>
</class>

等價的注釋:

@Entity
public class Item {
    @ManyToOne(fetch = FetchType.EAGER)
    private User seller;
    
    @OneToMany(fetch = FetchType.EAGER)
    private Set<Bid> bids = new HashSet<Bid>();
}

現在執行createCriteria(Item.class).list(),為執行以下SQL:

select i.*, b.* from ITEM i left outer join BID b on i.ITEM_ID = b.ITEM_ID

最后需要知道參數hibernate.max_fetch_depth,他控制最多可連接的關聯實體表個數,默認是沒有限制的,該值通常控制在1-5。

Finally, we have to introduce a global Hibernate configuration setting that you can use to control the maximum number of joined entity associations (not collections)注意只針對關聯實體,不針對集合.
The number of tables joined in this case depends on the global hibernate.max_fetch_depth configuration property.

Optimizing fetching for secondary tables

當查詢繼承實體對象時,SQL的處理可能更加復雜。如果繼承映射選擇的是table-per-hierarchy,則查詢在一條SQL中就可以完成。
CreditCardBankAccountBillingDetails的兩個子類。

List result = session.createQuery("from BillingDetails").list();
Outer joins for a table-per-subclass hierarchy

如果繼承映射選擇的是table-per-subclass,所有的子類都通過OUTER JOIN在一條SQL中查詢出來。

SELECT b1.BILLING_DETAILS_ID,
       b1.OWNER,
       b1.USER_ID,
       b2.NUMBER,
       b2.EXP_MONTH,
       b2.EXP_YEAR,
       b3.ACCOUNT,
       b3.BANKNAME,
       b3.SWIFT,
       CASE
         WHEN b2.CREDIT_CARD_ID IS NOT NULL THEN 1
         WHEN b3.BANK_ACCOUNT_ID IS NOT NULL THEN 2
         WHEN b1.BILLING_DETAILS_ID IS NOT NULL THEN 0
       END AS clazz
  FROM BILLING_DETAILS b1
  LEFT OUTER JOIN CREDIT_CARD b2 ON b1.BILLING_DETAILS_ID = b2.CREDIT_CARD_ID
  LEFT OUTER JOIN BANK_ACCOUNT b3 ON b1.BILLING_DETAILS_ID = b3.BANK_ACCOUNT_ID
Switching to additional selects

此節忽略,基本用不到,某些數據庫可能會限制連接表的數量,此時可以從JOIN切換到additional SELECT,即做完第一次查詢后,再立即發送一條SELECT語句來查詢。

Optimization guidelines

Hibernate默認抓取策略是fetch on demand,這就可能導致執行非常多的SELECT語句。
如果你將抓取策略設置為eager fetch with join,雖然只需要一條SELECT語句,但是可能會產生另外一個問題笛卡兒積 - Cartesian product,導致加載過多數據到內存以及persistence context中(尤其是eager join collection時)。

你需要找到一個平衡點,設置合適的fetch strategy;你需要知道哪些fetch plan and strategy應該設置為全局的,哪些策略應用與特定的查詢(HQL or Criteria)。

The n+1 selects problem

假如你現在使用Hibernatel默認的fetch on demand,當執行以下代碼:

List<Item> allItems = session.createQuery("from Item").list();
// List<Item> allItems = session.createCriteria(Item.class).list();

Map<Item, Bid> highestBids = new HashMap<Item, Bid>();
for (Item item : allItems) {
    Bid highestBid = null;
    for (Bid bid : item.getBids() ) { // Initialize the collection
        if (highestBid == null)
            highestBid = bid;
        if (bid.getAmount() > highestBid.getAmount())
            highestBid = bid;
    }
    highestBids.put(item, highestBid);
}

首先查詢所有的Item對象,不管是HQL還是Criteria的方式,Hibernate會執行一條SELECT語句,查詢出所有的Item對象,這里假如是N個。
然后循環處理Item list,獲取每個Item的bids collection,由于使用的fetch on demand策略,所以bids collection需要被初始化,所以Hibernate會再執行一條SELECT來初始化bids collection;這樣就總共執行了N + 1條SELECT查詢,這就是所謂有N + 1問題。

解決方法:

  1. 使用之前講到的prefetch in batch,如果batch size設置為10,那很可能會執行N/10 + 1條SELECT。
  2. 使用之前講到的prefetch with subselect,這樣就只會執行兩條SELECT。
  3. 前面兩種prefetch還是有一定的延遲效果的;現在徹底放棄lazy loading,使用eager fetching with join,這樣就只會執行一條SELECT。但是這種方式,在一條SELECT中OUTER JOIN collection時,很可能造成嚴重的笛卡爾積問題,所是不適合做為global mapping。

實際上,做為全局的映射(global mapping),應該選擇prefetch in batch來避免N+1問題;如果在某些case下,你確實需要關聯的collection而又不想在global mapping中設置eager fetching with join,可以使用如下方法:

// 在HQL中直接使用left join fetch,直接查詢出所有有的關聯bids collection
List<Item> allItems = session.createQuery("from Item i left join fetch i.bids").list();

// Criteria方式,效果同上
List<Item> allItems = session.createCriteria(Item.class).setFetchMode("bids", FetchMode.JOIN).list();

這兩種方式沒有使用全局策略,針對特定case利用HQL/Criteria(可以稱為dynamic fetching strategy)來避免N+1問題。

eager fetching with join對于<many-to-one>或<one-to-one>這種association,是種比較好的避免N+1問題的策略,因為這種to-one的關聯不會造成笛卡爾積問題。

The Cartesian product problem

當對collection(即一對多)使用eager fetching with join策略時,就會產生笛卡爾積問題,所以要避免在@OneToMany時使用eager fetching with join

如下映射,Item中存在兩個集合,并且都設置為eager fetching with join.

<class name="Item">
    <set name="bids" inverse="true" fetch="join">
        <key column="ITEM_ID"/>
        <one-to-many class="Bid"/>
    </set>

    <set name="images" fetch="join">
        <key column="ITEM_ID"/>
        <composite-element class="Image">
    </set>
</class>

執行SQL如下:

select item.*, bid.*, image.*
    from ITEM item
        left outer join BID bid on item.ITEM_ID = bid.ITEM_ID
        left outer join ITEM_IMAGE image on item.ITEM_ID = image.ITEM_ID

此時就會產生嚴重的笛卡爾積問題,導致查詢結果集中存在大量冗余數據。


Forcing proxy and collection initialization

除了利用全局的抓取策略和dynamic fetching strategy (利用HQL/Criteria),還有一種方式可以強制初始化proxy or collection wrapper.

Session session = sessionFactory.openSession();
Transaction tx = session.beginTransaction();

Item item = (Item) session.get(Item.class, new Long(1234));
Hibernate.initialize(item.getSeller()); // 強制初始化seller proxy

tx.commit();
session.close();
processDetached( item.getSeller() );

Hibernate.initialize()可以強制初始化proxy or collection wrapper,但是針對collection,集合中的每一個引用也只是初始化為一個proxy,不會真正初始化每個集合中的引用。

Explicit initialization with this static helper method is rarely necessary; you should always prefer a dynamic fetch with HQL or Criteria.

Optimization step by step

這節沒啥東西,開發時,開啟下面兩個參數

hibernate.format_sql
hibernate.use_sql_comments

此文是對《Java Persistence with Hibernate》第13章fetch部分的歸納。原文中有些XML mapping沒有列出,重點關注annotation的用法。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Joins, reporting queries, and subselects 在抓取策略這篇文章中有提到dyn...
    ilaoke閱讀 3,568評論 0 3
  • 本文包括:1、Hibernate 的查詢方式2、HQL (Hibernate Query Language) 查詢...
    廖少少閱讀 2,688評論 0 15
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,765評論 18 399
  • 延遲加載和急加載 有些時候,必須決定應該講那些數據從數據庫中加載到內存中,當執行entityManager.fin...
    Captain_w閱讀 667評論 0 0
  • 周末,妻收拾衣服,她把一件天藍色的襯衣拿出來,笑著對我說,還記得這件襯衣嗎?我說,不記得了,不知道什么時間買的了?...
    往事不再隨風閱讀 574評論 6 6