1. 程式人生 > >JPA] 效能優化: 4種觸發懶載入的方式

JPA] 效能優化: 4種觸發懶載入的方式

在一個JPA應用中,可以通過懶載入來提高應用的效能。這一點毋庸置疑,但是懶載入不等於不載入,在某個時刻還是需要載入這些資料的,那麼如何觸發這個載入的行為才能夠事半功倍呢?

這裡我想說一點題外話,面試的時候我也會考察被面試者對於JPA/Hibernate的看法,得到的答覆通常都包含了對JPA/Hibernate的一些”鄙夷”,比如JPA/Hibernate效能太菜了,現在主流的持久層框架是MyBatis云云。然後我也會試圖讓他們去分析一下為什麼會慢,一部分人不知道如何回答,答曰感覺很慢;另外一些人可能使用的經驗稍微多一些,於是答道JPA Provider(例如Hibernate)會生成非常多效率低下的SQL,於是看起來效能就不行了。如果再深究下去問如何改進這些效率低下的SQL呢?能夠談出個一二三的就更是鳳毛麟角了。所以我覺得,很多人其實在沒有深入調研JPA之前就給出了一個不是很恰當的結論。每種技術都有自身的優缺點,完美的技術是不存在的。具體問題具體分析,不要人云亦云是一個開發人員應該擁有的基本能力。當然我也不是JPA的衛道士,JPA在目前網際網路海量資料的環境下,確實有很多的問題,最典型的比如對於資料分片,分表分庫上支援的欠缺。我想正是這一點才讓MyBatis成為了目前的網際網路公司的主流選擇。但是,並不是所有的應用都有那麼大的資料量,也不是所有的專案都需要去分表分庫。更多的中小型專案,如果能夠合理地運用好JPA,開發效率和專案的服務效能絕對不會差。畢竟,JPA作為JavaEE標準的一部分,豈能浪得虛名?

所以,這篇文章我想從觸發懶載入這個角度,分析幾種不同的實現方式,來看看應該如何提高應用的效能。

0. 資料關聯關係的假設

在具體分析4種觸發方式之前,我們先來假設一組關聯關係:

@Entity
public class Department {
    // 主鍵等欄位
    // ......

    @OneToMany(mappedBy = "department")
    private List<Employee> employees;
}

1. 通過方法呼叫觸發

這是使用頻率最高的一種觸發方式,幾乎所有JPA開發人員一般情況下都會使用這種方式。甚至在任何場合下都使用這種方式的開發人員也不在少數。

顧名思義,這種方式通過在employee集合物件上呼叫方法來完成觸發,比如下面的程式碼:

Department dept = em.find(Department.class, deptId);
int count = dept.getEmployees().size();
// ......

通過呼叫size方法來觸發懶載入,這個size的執行會讓JPA的Provider生成具體去獲取集合資料的SQL並執行之。這種方法看似沒什麼問題,在很多場景下確實也非常好用。但是它太簡單粗暴了。在下面兩種情況下,都會造成較為嚴重的效能問題:

  1. 集合資料量大。比如關聯資料有上成百上千條記錄時。
  2. 一個實體型別需要觸發懶載入的關係很多。比如當上述Department型別還需要載入更多一對多的關係時。

第一種情況很好理解,資料量越大,SQL執行的時間越久,這一點毫無疑問。
第二種情況,假設Department型別有10個一對多關係,現在都需要觸發懶載入行為來得到完整的資料。那麼針對每個關係都會產生一條SQL命令。加上它自身的,一共就是11條命令。當然你的應用往往不會只有一個使用者在使用,假設有100個使用者同時使用,那麼就是1100條SQL會被執行!這能不慢嗎?

所以對於這種觸發方式,在確定不會發生上述兩種情況時,是可以使用的。一旦有發生它們的風險,就不要使用了。

2. 通過Join Fetch觸發

這種方式通過在JPQL中新增JOIN FETCH來完成關聯關係的獲取:

Query q = em.createQuery("SELECT d FROM Department d JOIN FETCH d.employees e WHERE d.id = :id");
q.setParameter("id", deptId);
Department dept = (Department) q.getSingleResult();

這種方式,主要解決了在通過方法呼叫觸發時面臨的第二個問題:執行的SQL命令數量過多。
使用JOIN FETCH時,執行的SQL命令只有一條。因此,需要觸發載入行為的關係越多,使用JOIN FETCH帶來的效能優勢就越明顯。

但是這種方式並非一本萬利,如果不同業務場景下需要觸發載入的關係不一樣,就會產生非常多的組合。而每種組合的JPQL都是不一樣的。此時可以結合實際的業務需求通過字串的拼接操作完成JPQL的準備工作。而這個準備工作在組合情況很多的情況下,往往會十分複雜。不過相比它能夠帶來的效能提升,這些麻煩都是可以克服的。

除了直接提供JPQL,在Criteria API中也能使用:

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery q = cb.createQuery(Department.class);
Root d = q.from(Department.class);
d.fetch("employees", JoinType.INNER);
q.select(d);
q.where(cb.equal(d.get("id"), deptId));
Department dept = (Department)em.createQuery(q).getSingleResult();

這種方式只是定義查詢的方式不同而已,在效能層面上和直接寫JPQL是一樣的。

3. 通過NamedEntityGraph觸發

這種方式實際上是JPA 2.1中新增的一項特性。藉助它也能夠完成懶載入的觸發。我們先來看看如何定義一個命名的EntityGraph,即NamedEntityGraph。從命名方式上看,是不是很接近於NamedEntityQuery?所以引申到定義方式而言,它們也是很接近的:

@Entity
@NamedEntityGraph(name = "graph.Department.employees", 
      attributeNodes = @NamedAttributeNode("employees"))
public class Department {
    // ......
}

使用它進行關係的載入:

EntityGraph graph = em.getEntityGraph("graph.Department.employees");
Map<String, Object> props = new HashMap<>();
props.put("javax.persistence.fetchgraph", graph);
Department dept = em.find(Department.class, deptId, props);

此時查詢得到的Department物件就包含了我們需要的Employee集合。同樣地,使用這種方式的時候也只會生成並執行一條SQL命令。

但是在組合情況比較多的時候,和使用Join Fetch一樣,也是需要根據業務場景進行一些準備工作的,只不過這個準備工作更加麻煩,每個組合都需要新增一個專門的@NamedEntityGraph註解用來定義。所以,在組合關係很多的時候,使用@NamedEntityGraph是很不划算的。因此也就有了下面的動態EntityGraph。

4. 通過動態的EntityGraph觸發

動態EntityGraph的定義方式更加靈活:

EntityGraph graph = this.em.createEntityGraph(Department.class);
Subgraph employeesGraph = graph.addSubgraph("employees");
Map<String, Object> props = new HashMap<>();
props.put("javax.persistence.loadgraph", graph);
Department dept = em.find(Department.class, deptId, props);

動態EntityGraph根據需要載入的關係,通過addSubgraph方法進行指定。

5. EntityGraph與JOIN FETCH是一樣的?

細心的同學看到這裡,也許會發現目前介紹的所謂EntityGraph,怎麼跟JOIN FETCH那麼那麼像呢?難道只是換了個馬甲?

很顯然,並沒有這麼簡單。

注意一下上面的這兩行程式碼:

// #1 loadgraph
props.put("javax.persistence.loadgraph", graph);

// #2 fetchgraph
props.put("javax.persistence.fetchgraph", graph);

這兩者有什麼具體的區別呢?這裡我不打算長篇累牘地介紹。撿重點說就是:

  1. loadgraph:在原有Entity的定義的基礎上,定義需要獲取什麼欄位/關係
  2. fetchgraph:完全放棄原有Entity的定義,定義需要獲取什麼欄位/關係

注意上面的”還”和”僅”,表達了兩者最大的不同點。

舉個例子,如果我們的Department型別中還有一個name欄位:

  1. loadgraph:被載入的資料為name以及employees
  2. fetchgraph:被載入的資料僅為employees

所以,在使用EntityGraph的時候配合fetchgraph,可以精準的完成所需要資料的載入,可謂是指哪打哪。在一些對效能尤其敏感的業務場景,不妨來試試看僅僅載入所需要的資料的那種酸爽吧。

關於此話題的更多資源,可以參考官方文件

結語

分析比較了以上這4種用於觸發懶載入的方式,我們應該有能力辨別出在什麼場景下使用哪種方式更適合了。還不快去在你的工程中嚐嚐鮮?