Spring Data JPA為Spring應用程序提供了數據訪問層的實現。這是一個非常方便的組件,因為它不會重新發明每個新應用程序的數據訪問方式,因此您可以花更多時間來實現業務邏輯。使用Spring Data JPA時有一些好的做法。例如,限制不必要的對象的加載以優化性能。
本文將為您提供一些技巧,以減少請求數據庫的次數,而不是檢索數據庫的所有元素,因此不會影響應用程序的整體性能。為此,我們首先會看到Spring Data JPA提供的各種工具來改進對數據訪問的控制,以及一些良好的實踐去減少數據檢索對我們應用程序的影響的。然后,我將與您分享一個具體的例子,通過在這些不同的方面發揮作用來提高Spring應用程序的性能,從而減少潛在的問題。
實體關系的加載
當使用Spring Data JPA創建應用程序(並且通常使用Hibernate)時,可能會自動加載對象依賴關系(例如書的作者)- EAGER;或手動加載 - LAZY。
使用EAGER類型依賴關系時,每次加載對象時,都會加載相關對象:當您詢問書籍數據時,也會檢索作者的數據。
使用LAZY類型依賴關系時,只會加載所需對象的數據:不會檢索作者的數據。
使用Spring Data JPA,2個域對象之間的每個關系都擁有這些數據加載類型之一。默認情況下,該方法將由關系類型確定。
以下是與其數據加載默認類型的所有可能關系:
@OneToOne
對於實體A的每個實例,實體B的一個(並且僅有一個)實例被關聯。B也僅與實體A的一個實例相關聯。
一個典型的例子是患者和他的記錄之間的關系:
@Entity public class Patient implements Serializable { @OneToOne private PatientRecord record; }
對於這種關系類型,默認的數據加載方法是EAGER:每次詢問患者的數據時,患者記錄的數據也將被檢索。
@ManyToOne
對於實體A的每個實例,有一個實體B(並且僅有一個)實例被關聯。另一方面,B可能與A的許多實例相關聯。
一個典型的例子是產品及其類別之間的關系:
@Entity public class Product implements Serializable { @ManyToOne private ProductCategory category; }
對於這種關系類型,默認數據加載方法是EAGER:每次詢問產品數據時,該類別的數據也將被檢索。
@OneToMany
對於實體A的每個實例會關聯實體B的零個、一個或多個實例。另一方面,B僅鏈接到A的一個實例。
這與@ManyToOne關系相反,所以一個典型的例子可能是產品類別及其關聯產品列表:
@Entity public class ProductCategory implements Serializable { @OneToMany private Set<Product> products = new HashSet<>(); }
對於這種關系類型,默認數據加載方法是LAZY:每次詢問類別數據時,產品列表都不會被檢索。
@ManyToMany
對於實體A的每個實例會關聯實體B的零個、一個或多個實例。相反的情況也是如此,B與零個、一個或多個A的實例相關聯。
一個典型的例子是博客文章與其主題列表之間的關系:
@Entity public class Article implements Serializable { @ManyToMany private Set<Topic> topics = new HashSet<>(); }
對於這種關系類型,默認數據加載方法是LAZY:每次請求文章數據時,主題列表都不會被檢索。
盡量減少EAGER關系的使用
目標是從數據庫中僅加載所需數據以獲取所要求的數據。例如,如果您想按應用程序中注冊的名稱顯示作者列表,則不需要獲取所有關系的數據:如他們編寫的書籍,地址等。
一個好的做法是盡量減少自動加載的關系(例如,避免使用Eager),事實上,你擁有EAGER關系越多,你將獲取的對象就越不一定有用。這意味着增加了數據庫所需的往返次數,並增加了專用於數據庫表到應用程序實體之間映射的時間。因此,優先使用LAZY關系並只在需要時才加載對應關系的數據可能會更好。
具體而言,建議只使用EAGER加載那些你確定始終會用的的關系數據(我認為它並不常見)。這意味着要為兩個關系@OneToMany和@ManyToMany設置默認的加載方法,並為兩個關系@OneToOne和@ManyToOne強制加載LAZY。這反映在指定關系的fetch屬性中:
@Entity public class Product implements Serializable { @ManyToOne(fetch = FetchType.LAZY) private ProductCategory category; }
這需要對每個實體和每個關系進行額外的調整工作,為此我們需要創建新的方法,這些方法將允許我們在最少的查詢中加載所需的所有數據。事實上,如果需要顯示與作者有關的所有數據(他的書目錄,他的地址等),那么在一次查詢中獲取該對象及其關系將會很重要,可以使用join去解決。
如何控制會執行哪些查詢
Spring Data JPA為我們提供了訪問數據的途徑。但是,您必須知道這些是如何實現的。要驗證執行哪些查詢以從數據庫檢索數據,必須啟動Hibernate的日志。
其中一個方式是添加如下配置
spring: jpa: show-sql: true
如何優化LAZY對象的檢索
Spring Data JPA提供了指定在數據庫中的選擇查詢期間將加載哪些關系的功能。我們將用幾種方法來看看相同的示例:如何在單個查詢中檢索包含其主題的文章。
方法1:使用@Query檢索和加載對象
@Query注解允許使用JPQL語言編寫選擇查詢。因此,您可以使用join中使用JPQL關鍵字fetch 來加載這些關系。
在Article實體中,可以通過指定在檢索到的文章的實例中加載主題列表來創建方法findOneWithTopicsById:
@Repository public interface ArticleRepository extends JpaRepository<Article,Long> { @Query("select article from Article article left join fetch article.topics where article.id =:id") Article findOneWithTopicsById(@Param("id") Long id); }
方法2:使用@EntityGraph檢索和加載對象
從Spring Data JPA的1.10版開始,您可以使用@EntityGraph注解來創建關系圖,以在請求時加載實體。
這個注解也被用在JPA repositories中。可以直接在repository的查詢上或實體上定義。
在查詢中定義
我們定義了由關鍵字attributePaths代表關系列表的關系(這里是一個元素的列表):
@Repository public interface ArticleRepository extends JpaRepository<Article,Long> { @EntityGraph(attributePaths = "topics") Article findOneWithTopicsById(Long id); }
在實體中定義
從JPA 2.1開始,可以使用命名實體視圖在實體上定義這些圖。主要優點是可以在多個查詢中使用此視圖。在這種情況下,我們使用關鍵字attributeNodes指定加載關系的列表,該關鍵字是@NamedAttributeNode的列表。下面看下如何在Article實體中實現它。
@Entity @NamedEntityGraph(name = "Article.topics", attributeNodes = @NamedAttributeNode("topics")) public class Article implements Serializable { ... }
可以按照如下的方式使用它:
@Repository public interface ArticleRepository extends JpaRepository<Article,Long> { @EntityGraph(value = "Article.topics") Article findOneWithTopicsById(Long id); }
此外,可以使用屬性類型指定非指定關系的加載類型:對所有非指定關系進行LAZY加載或默認加載。
也可以創建子圖,從而以分層方式工作以盡可能的減少不比較的加載。
@EntityGraph的使用限制
對於這兩種與實體圖相關的方法,我們不能檢索包含所有具有關系的實體的列表。事實上,我們可能想創建一個方法,例如將其定義為findAllWithTopics(),來獲取所有的文章和文章對應的主題 列表。這是不可以的,必須使用搜索限制(如使用where來篩選)。
為了克服這個限制,一個解決方案是創建一個方法findAllWithTopicsByIdNotNull():該id永遠不為null,所有的數據將被檢索。另一種方法是使用第一種方法@Query來執行join查詢,因為@Query注解沒有此限制。
如果有必要可以添加非可選信息
當@OneToOne或@ManyToOne關系是強制性的 - 也就是說,實體必須具有關聯關系 - 告知Spring Data JPA這種關系不是可選的,是很重要的。
我們可以舉一個例子:一個人必須有一個地址,它本身可以被幾個人共享。所以,關系的定義如下:
@Entity public class Person implements Serializable { @ManyToOne(optional = false) @NotNull private Adress adress; }
添加optional = false信息將允許Spring Data JPA在創建其查詢語句時更高效,因為它知道person一定會關聯一個非空的地址。因此,在定義強制關系時始終指定此屬性是一種好的做法。
注意事項
盡管將關系從EAGER加載換成LAZY加載可能會提高性能,但它也會產生一些意想不到的后果,並且可能會出現一些錯誤。這里有兩個很常見的例子。
可能的信息丟失
第一個副作用可能是信息丟失,例如通過Web服務發送實體時。
例如,當我們修改Person和Address之間從EAGER到LAZY之間的關系時,我們必須重新檢查獲取Person實體的查詢語句,將address屬性一起查詢出來(使用上述方法之一)。否則,Person Web服務可能僅提供特定於該Person的數據,並且address數據可能已丟失。
這是一個問題,因為Web服務可能不再滿足其接口協議。例如,它可能會影響網頁上的顯示:需要將地址顯示在HTML中。
為了避免這個問題,使用數據傳輸對象(DTO)而不是直接將實體返回給客戶端是很有必要的。實際上,映射器將實體轉換為DTO將通過在數據庫中檢索初始查詢期間尚未加載的關系來加載它需要的所有關系:這就是Lazy Loading。因此,即使我們不重構實體,Web服務也將繼續返回相同的數據。
可能的事務問題
第二個副作用可以是LazyInitializationException。
嘗試在事務外加載關系時發生此異常。無法完成懶加載,因為對象已分離:它不再屬於Hibernate會話。當把EAGER改為LAZY時,可能會發生這個異常。
在這種情況下,可能出現的例外情況有兩個主要原因:
- 第一個原因可能是你不在Hibernate事務中。在這種情況下,您必須使流程事務化(在該方法或其類上設置@Transactional注接)或調用可以負責加載依賴關系的事務服務。
- 第二個原因可能是在事務之外處理了懶加載對象,且該實體未附加到您的新事務中。在這種情況下,您必須在第一個事務中加載關系,或者在第二個事務中重新連接對象。
分頁查詢的特性
當你想創建一個包含來自一個或多個關系信息的分頁查詢時,直接從一個查詢語句中就加載其關聯的屬性的數據是一個(非常)不好的主意。例如,當我們檢索包含主題的文章的第一頁時,最好不要直接加載所有文章+主題數據,而是首先加載文章中的數據,然后加載與主題相關的數據。
事實上,如果不這樣做,應用程序將被迫建立2個表之間的的完整數據集,將它們存儲在內存中,然后只選擇所請求頁面的數據。這與數據庫的工作方式直接相關:即使我們只需要數據的一部分(一頁數據,最小/最大的結果,結果的前幾個),它們也必須join的所有數據。
在加載頁面及其關系的情況下,日志中會顯示一條明確的消息來警告您:
HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!
2個表的體積越大,影響越大:對於包含數百萬個條目的表,這可能導致應用程序的處理非常昂貴。
為了克服這個問題,首先加載不包含關系的實體,然后在第二步中再次加載相關聯的實體。要加載請求頁面的實體數據,可以使用JPA存儲庫的findAll(Pageable pageable)方法(從PagingAndSortingRepository類繼承),然后,要加載關系數據,可以使用Lazy Loading直接調用關系的getter來檢索每個實體的數據(需要在事務中)。
但是這個操作會很昂貴,因為它會產生很多查詢。事實上,對於每個實體,將會有許多選擇查詢,因為有加載關系:這個問題被稱為“Hibernate N + 1查詢問題”。如果我們以加載包含關系的20篇文章的頁面為例進行加載,則這將導致21個查詢:頁面為1,每篇文章的主題為20。
為了降低成本,可以在兩個實體之間的@OneToMany或@ManyToMany關系上使用@BatchSize(size = n)注釋。這允許Hibernate在數據庫中進行選擇查詢之前等待足夠的(n個)關系進行檢索。這個數字n要與頁面的大小相關聯(但它意味着有一個默認的頁面大小,因為n是在實體上定義的,因此是恆定的)。在前面的例子中,我們可以將最小數字指定為20:
@Entity public class Article implements Serializable { @ManyToMany @BatchSize(size = 20) private Set<Topic> topics = new HashSet<>(); }
在這種情況下,加載頁面的查詢數將從21減少到2,一次加載所有文章,另一次加載所有主題。
注意:如果頁面包含少於20個元素(小於n),則主題仍將被正確加載。
原文鏈接 https://blog.ippon.tech/boost-the-performance-of-your-spring-data-jpa-application/
