扼殺性能的10個(gè)常見(jiàn)Hibernate錯(cuò)誤
你有沒(méi)有想過(guò)如果你能解決Hibernate問(wèn)題,那么你的應(yīng)用程序可以更快?
那么請(qǐng)閱讀這篇文章!
我在很多應(yīng)用程序中修復(fù)過(guò)性能問(wèn)題,其中大部分都是由同樣的錯(cuò)誤引起的。修復(fù)之后,性能變得更溜,而且其中的大部分問(wèn)題都很簡(jiǎn)單。所以,如果你想改進(jìn)應(yīng)用程序,那么可能也是小菜一碟。
這里列出了導(dǎo)致Hibernate性能問(wèn)題的10個(gè)最常見(jiàn)的錯(cuò)誤,以及如何修復(fù)它們。

錯(cuò)誤1:使用Eager Fetching
FetchType.EAGER的啟示已經(jīng)討論了好幾年了,而且有很多文章對(duì)它進(jìn)行了詳細(xì)的解釋。我自己也寫了一篇。但不幸的是,它仍然是性能問(wèn)題最常見(jiàn)的兩個(gè)原因之一。
FetchType定義了Hibernate何時(shí)初始化關(guān)聯(lián)。你可以使用@OneToMany,@ManyToOne,@ManyToMany和@OneToOneannotation注釋的fetch屬性進(jìn)行指定。
- @Entity
- public class Author{
- @ManyToMany(mappedBy="authors", fetch=FetchType.LAZY)
- private List<Book> books = new ArrayList<Book>();
- ...
- }
當(dāng)Hibernate加載一個(gè)實(shí)體的時(shí)候,它也會(huì)即時(shí)加載獲取的關(guān)聯(lián)。例如,當(dāng)Hibernate加載Author實(shí)體時(shí),它也提取相關(guān)的Book實(shí)體。這需要對(duì)每個(gè)Author進(jìn)行額外的查詢,因此經(jīng)常需要幾十甚至數(shù)百個(gè)額外的查詢。
這種方法是非常低效的,因?yàn)镠ibernate不管你是不是要使用關(guān)聯(lián)都會(huì)這樣做。***改用FetchType.LAZY代替。它會(huì)延遲關(guān)系的初始化,直到在業(yè)務(wù)代碼中使用它。這可以避免大量不必要的查詢,并提高應(yīng)用程序的性能。
幸運(yùn)的是,JPA規(guī)范將FetchType.LAZY定義為所有對(duì)多關(guān)聯(lián)的默認(rèn)值。所以,你只需要確保你不改變這個(gè)默認(rèn)值即可。但不幸的是,一對(duì)一關(guān)系并非如此。
錯(cuò)誤2:忽略一對(duì)一關(guān)聯(lián)的默認(rèn)FetchType
接下來(lái),為了防止立即抓取(eager fetching),你需要做的是對(duì)所有的一對(duì)一關(guān)聯(lián)更改默認(rèn)的FetchType。不幸的是,這些關(guān)系在默認(rèn)情況下會(huì)被即時(shí)抓取。在一些用例中,那并非一個(gè)大問(wèn)題,因?yàn)槟阒皇羌虞d了一個(gè)額外的數(shù)據(jù)庫(kù)記錄。但是,如果你加載多個(gè)實(shí)體,并且每個(gè)實(shí)體都指定了幾個(gè)這樣的關(guān)聯(lián),那么很快就會(huì)積少成多,水滴石穿。
所以,***確保所有的一對(duì)一關(guān)聯(lián)設(shè)置FetchType為L(zhǎng)AZY。
- @Entity
- public class Review {
- @ManyToOne(fetch = FetchType.LAZY)
- @JoinColumn(name = "fk_book")
- private Book book;
- ...
- }
錯(cuò)誤3:不要初始化所需的關(guān)聯(lián)
當(dāng)你對(duì)所有關(guān)聯(lián)使用FetchType.LAZY以避免錯(cuò)誤1和錯(cuò)誤2時(shí),你會(huì)在代碼中發(fā)現(xiàn)若干n+1選擇問(wèn)題。當(dāng)Hibernate執(zhí)行1個(gè)查詢來(lái)選擇n個(gè)實(shí)體,然后必須為每個(gè)實(shí)體執(zhí)行一個(gè)額外的查詢來(lái)初始化一個(gè)延遲的獲取關(guān)聯(lián)時(shí),就會(huì)發(fā)生這個(gè)問(wèn)題。
Hibernate透明地獲取惰性關(guān)系,因此在代碼中很難找到這種問(wèn)題。你只要調(diào)用關(guān)聯(lián)的getter方法,我想我們大家都不希望Hibernate執(zhí)行任何額外的查詢吧。
- List<Author> authors = em.createQuery("SELECT a FROM Author a", Author.class).getResultList();
- for (Author a : authors) {
- log.info(a.getFirstName() + " " + a.getLastName() + " wrote "
- + a.getBooks().size() + " books.");
- }
如果你使用開發(fā)配置激活Hibernate的統(tǒng)計(jì)組件并監(jiān)視已執(zhí)行的SQL語(yǔ)句的數(shù)量,n+1選擇問(wèn)題就會(huì)更容易被發(fā)現(xiàn)。
- 15:06:48,362 INFO [org.hibernate.engine.internal.StatisticalLoggingSessionEventListener] - Session Metrics {
- 28925 nanoseconds spent acquiring 1 JDBC connections;
- 24726 nanoseconds spent releasing 1 JDBC connections;
- 1115946 nanoseconds spent preparing 13 JDBC statements;
- 8974211 nanoseconds spent executing 13 JDBC statements;
- 0 nanoseconds spent executing 0 JDBC batches;
- 0 nanoseconds spent performing 0 L2C puts;
- 0 nanoseconds spent performing 0 L2C hits;
- 0 nanoseconds spent performing 0 L2C misses;
- 20715894 nanoseconds spent executing 1 flushes (flushing a total of 13 entities and 13 collections);
- 88175 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections)
- }
正如你所看到的JPQL查詢和對(duì)12個(gè)選定的Author實(shí)體的每一個(gè)調(diào)用getBooks方法,導(dǎo)致了13個(gè)查詢。這比大多數(shù)開發(fā)人員所以為的還要多,在他們看到如此簡(jiǎn)單的代碼片段的時(shí)候。
如果你讓Hibernate初始化所需的關(guān)聯(lián),那么你可以很容易地避免這種情況。有若干不同的方式可以做到這一點(diǎn)。最簡(jiǎn)單的方法是添加JOIN FETCH語(yǔ)句到FROM子句中。
- Author a = em.createQuery(
- "SELECT a FROM Author a JOIN FETCH a.books WHERE a.id = 1",
- Author.class).getSingleResult();
錯(cuò)誤4:選擇比所需的更多記錄
當(dāng)我告訴你選擇太多的記錄會(huì)減慢應(yīng)用程序的速度時(shí),我敢保證你一定不會(huì)感到驚訝。但是我仍然經(jīng)常會(huì)發(fā)現(xiàn)這個(gè)問(wèn)題,當(dāng)我在咨詢電話中分析應(yīng)用程序的時(shí)候。
其中一個(gè)原因可能是JPQL不支持你在SQL查詢中使用OFFSET和LIMIT關(guān)鍵字。這看起來(lái)似乎不能限制查詢中檢索到的記錄數(shù)量。但是,你可以做到這一點(diǎn)。你只需要在Query接口上,而不是在JPQL語(yǔ)句中設(shè)置此信息。
我在下面的代碼片段中做到這一點(diǎn)。我首先通過(guò)id排序選定的Author實(shí)體,然后告訴Hibernate檢索前5個(gè)實(shí)體。
- List<Author> authors = em.createQuery("SELECT a FROM Author a ORDER BY a.id ASC", Author.class)
- .setMaxResults(5)
- .setFirstResult(0)
- .getResultList();
錯(cuò)誤5:不使用綁定參數(shù)
綁定參數(shù)是查詢中的簡(jiǎn)單占位符,并提供了許多與性能無(wú)關(guān)的好處:
- 它們非常易于使用。
- Hibernate自動(dòng)執(zhí)行所需的轉(zhuǎn)換。
- Hibernate會(huì)自動(dòng)轉(zhuǎn)義Strings,防止SQL注入漏洞。
而且也可以幫助你實(shí)現(xiàn)一個(gè)高性能的應(yīng)用程序。
大多數(shù)應(yīng)用程序執(zhí)行大量相同的查詢,只在WHERE子句中使用了一組不同的參數(shù)值。綁定參數(shù)允許Hibernate和數(shù)據(jù)庫(kù)識(shí)別與優(yōu)化這些查詢。
你可以在JPQL語(yǔ)句中使用命名的綁定參數(shù)。每個(gè)命名參數(shù)都以“:”開頭,后面跟它的名字。在查詢中定義了綁定參數(shù)后,你需要調(diào)用Query接口上的setParameter方法來(lái)設(shè)置綁定參數(shù)值。
- TypedQuery<Author> q = em.createQuery(
- "SELECT a FROM Author a WHERE a.id = :id", Author.class);
- q.setParameter("id", 1L);
- Author a = q.getSingleResult();
錯(cuò)誤6:執(zhí)行業(yè)務(wù)代碼中的所有邏輯
對(duì)于Java開發(fā)人員來(lái)說(shuō),在業(yè)務(wù)層實(shí)現(xiàn)所有的邏輯是自然而然的。我們可以使用我們最熟悉的語(yǔ)言、庫(kù)和工具。
但有時(shí)候,在數(shù)據(jù)庫(kù)中實(shí)現(xiàn)操作大量數(shù)據(jù)的邏輯會(huì)更好。你可以通過(guò)在JPQL或SQL查詢中調(diào)用函數(shù)或者使用存儲(chǔ)過(guò)程來(lái)完成。
讓我們快速看看如何在JPQL查詢中調(diào)用函數(shù)。如果你想深入探討這個(gè)話題,你可以閱讀我關(guān)于存儲(chǔ)過(guò)程的文章。
你可以在JPQL查詢中使用標(biāo)準(zhǔn)函數(shù),就像在SQL查詢中調(diào)用它們一樣。你只需引用該函數(shù)的名稱,后跟一個(gè)左括號(hào),一個(gè)可選的參數(shù)列表和一個(gè)右括號(hào)。
- Query q = em.createQuery("SELECT a, size(a.books) FROM Author a GROUP BY a.id");
- List<Object[]> results = q.getResultList();
并且,通過(guò)JPA的函數(shù)function,你也可以調(diào)用數(shù)據(jù)庫(kù)特定的或自定義的數(shù)據(jù)庫(kù)函數(shù)。
- TypedQuery<Book> q = em.createQuery(
- "SELECT b FROM Book b WHERE b.id = function('calculate', 1, 2)",
- Book.class);
- Book b = q.getSingleResult();
錯(cuò)誤7:無(wú)理由地調(diào)用flush方法
這是另一個(gè)比較普遍的錯(cuò)誤。開發(fā)人員在持久化一個(gè)新實(shí)體或更新現(xiàn)有實(shí)體后,調(diào)用EntityManager的flush方法時(shí)經(jīng)常會(huì)出現(xiàn)這個(gè)錯(cuò)誤。這迫使Hibernate對(duì)所有被管理的實(shí)體執(zhí)行臟檢查,并為所有未決的插入、更新或刪除操作創(chuàng)建和執(zhí)行SQL語(yǔ)句。這會(huì)減慢應(yīng)用程序,因?yàn)樗柚沽薍ibernate使用一些內(nèi)部?jī)?yōu)化。
Hibernate將所有被管理的實(shí)體存儲(chǔ)在持久性上下文中,并試圖盡可能延遲寫操作的執(zhí)行。這允許Hibernate將同一實(shí)體上的多個(gè)更新操作合并為一個(gè)SQL UPDATE語(yǔ)句,通過(guò)JDBC批處理綁定多個(gè)相同的SQL語(yǔ)句,并避免執(zhí)行重復(fù)的SQL語(yǔ)句,這些SQL語(yǔ)句返回你已在當(dāng)前Session中使用的實(shí)體。
作為一個(gè)經(jīng)驗(yàn)法則,你應(yīng)該避免任何對(duì)flush方法的調(diào)用。JPQL批量操作是罕見(jiàn)的例外之一,對(duì)此我將在錯(cuò)誤9中解釋。
錯(cuò)誤8:使用Hibernate應(yīng)付一切
Hibernate的對(duì)象關(guān)系映射和各種性能優(yōu)化使大多數(shù)CRUD用例的實(shí)現(xiàn)非常簡(jiǎn)單和高效。這使得Hibernate成為許多項(xiàng)目的一個(gè)很好的選擇。但這并不意味著Hibernate對(duì)于所有的項(xiàng)目都是一個(gè)很好的解決方案。
我在我之前的一個(gè)帖子和視頻中詳細(xì)討論過(guò)這個(gè)問(wèn)題。JPA和Hibernate為大多數(shù)創(chuàng)建、讀取或更新一些數(shù)據(jù)庫(kù)記錄的標(biāo)準(zhǔn)CRUD用例提供了很好的支持。對(duì)于這些用例,對(duì)象關(guān)系映射可以大大提升生產(chǎn)力,Hibernate的內(nèi)部?jī)?yōu)化提供了一個(gè)很優(yōu)越的性能。
但是,當(dāng)你需要執(zhí)行非常復(fù)雜的查詢、實(shí)施分析或報(bào)告用例或?qū)Υ罅坑涗泩?zhí)行寫操作時(shí),結(jié)果就不同了。所有這些情況都不適合JPA和Hibernate的查詢能力以及基于實(shí)體管理的生命周期。
如果這些用例只占應(yīng)用程序的一小部分,那么你仍然可以使用Hibernate。但總的來(lái)說(shuō),你應(yīng)該看看其他的框架,比如jOOQ或者Querydsl,它們更接近于SQL,并且可以避免任何對(duì)象關(guān)系映射。
錯(cuò)誤9:逐個(gè)更新或刪除巨大的實(shí)體列表
在你看著你的Java代碼時(shí),感覺(jué)逐個(gè)地更新或刪除實(shí)體也可以接受。這就是我們對(duì)待對(duì)象的方式,對(duì)吧?
這可能是處理Java對(duì)象的標(biāo)準(zhǔn)方法,但如果你需要更新大量的數(shù)據(jù)庫(kù)記錄,那么,這就不是一個(gè)好方法了。在SQL中,你只需一次定義一個(gè)影響多個(gè)記錄的UPDATE或DELETE語(yǔ)句。數(shù)據(jù)庫(kù)將會(huì)非常高效地處理這些操作。
不幸的是,用JPA和Hibernate操作起來(lái)則沒(méi)有那么容易。每個(gè)實(shí)體都有自己的生命周期,而你如果要更新或刪除多個(gè)實(shí)體的話,則首先需要從數(shù)據(jù)庫(kù)加載它們。然后在每個(gè)實(shí)體上執(zhí)行操作,Hibernate將為每個(gè)實(shí)體生成所需的SQL UPDATE或DELETE語(yǔ)句。因此,Hibernate不會(huì)只用1條語(yǔ)句來(lái)更新1000條數(shù)據(jù)庫(kù)記錄,而是至少會(huì)執(zhí)行1001條語(yǔ)句。
很顯然,執(zhí)行1001條語(yǔ)句比僅僅執(zhí)行1條語(yǔ)句需要花費(fèi)更多的時(shí)間。幸運(yùn)的是,你可以使用JPQL、原生SQL或Criteria查詢對(duì)JPA和Hibernate執(zhí)行相同的操作。
但是它有一些你應(yīng)該知道的副作用。在數(shù)據(jù)庫(kù)中執(zhí)行更新或刪除操作時(shí),將不使用實(shí)體。這提供了更佳的性能,但它同時(shí)忽略了實(shí)體生命周期,并且Hibernate不能更新任何緩存。
在《How to use native queries to perform bulk updates》一文中對(duì)此我有一個(gè)詳細(xì)的解釋。
簡(jiǎn)而言之,在執(zhí)行批量更新之前,你不應(yīng)使用任何生命周期偵聽器以及在EntityManager上調(diào)用flush和clear方法。flush方法將強(qiáng)制Hibernate在clear方法從當(dāng)前持久化上下文中分離所有實(shí)體之前,將所有待處理的更改寫入數(shù)據(jù)庫(kù)。
- em.flush();
- em.clear();
- Query query = em.createQuery("UPDATE Book b SET b.price = b.price*1.1");
- query.executeUpdate();
錯(cuò)誤10:使用實(shí)體進(jìn)行只讀操作
JPA和Hibernate支持一些不同的projections。如果你想優(yōu)化你的應(yīng)用程序的性能,那么你應(yīng)該使用projections。最明顯的原因是你應(yīng)該只選擇用例中需要的數(shù)據(jù)。
但這不是唯一的原因。正如我在最近的測(cè)試中顯示的那樣,即使你讀取了相同的數(shù)據(jù)庫(kù)列,DTO projections也比實(shí)體快得多。
在SELECT子句中使用構(gòu)造函數(shù)表達(dá)式而不是實(shí)體只是一個(gè)小小的改變。但在我的測(cè)試中,DTO projections比實(shí)體快40%。當(dāng)然,兩者比較的數(shù)值取決于你的用例,而且你也不應(yīng)該通過(guò)這樣一個(gè)簡(jiǎn)單而有效的方式來(lái)提高性能。
了解如何查找和修復(fù)Hibernate性能問(wèn)題
正如你所看到的,一些小小的問(wèn)題都可能會(huì)減慢你的應(yīng)用程序。但幸運(yùn)的是,我們可以輕松避免這些問(wèn)題并構(gòu)建高性能持久層。






















