簡體   English   中英

JPA和Hibernate中N+1問題的解決方案是什么?

[英]What is the solution for the N+1 issue in JPA and Hibernate?

我知道 N+1 問題是執行一個查詢以獲取 N 條記錄和執行 N 個查詢以獲取一些關系記錄。

但是如何在 Hibernate 中避免它呢?

問題

當您忘記獲取關聯然后需要訪問它時,就會發生 N+1 查詢問題。

例如,假設我們有以下 JPA 查詢:

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    where pc.review = :review
    """, PostComment.class)
.setParameter("review", review)
.getResultList();

現在,如果我們迭代PostComment實體並遍歷post關聯:

for(PostComment comment : comments) {
    LOGGER.info("The post title is '{}'", comment.getPost().getTitle());
}

Hibernate 將生成以下 SQL 語句:

SELECT pc.id AS id1_1_, pc.post_id AS post_id3_1_, pc.review AS review2_1_
FROM   post_comment pc
WHERE  pc.review = 'Excellent!'

INFO - Loaded 3 comments

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 1

INFO - The post title is 'Post nr. 1'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 2

INFO - The post title is 'Post nr. 2'

SELECT pc.id AS id1_0_0_, pc.title AS title2_0_0_
FROM   post pc
WHERE  pc.id = 3

INFO - The post title is 'Post nr. 3'

這就是 N+1 查詢問題的生成方式。

因為在獲取PostComment實體時沒有初始化post關聯,所以 Hibernate 必須使用輔助查詢獲取Post實體,並且對於 N 個PostComment實體,將要執行 N 個更多的查詢(因此出現 N+1 查詢問題)。

修復

要解決此問題,您需要做的第一件事是添加 [適當的 SQL 日志記錄和監控][1]。 如果沒有日志記錄,您在開發某個功能時不會注意到 N+1 查詢問題。

其次,要修復它,您只需JOIN FETCH導致此問題的關系:

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    join fetch pc.post p
    where pc.review = :review
    """, PostComment.class)
.setParameter("review", review)
.getResultList();

如果您需要獲取多個子關聯,最好在初始查詢中獲取一個集合,並使用輔助 SQL 查詢獲取第二個集合。

如何自動檢測N+1查詢問題

這個問題最好通過集成測試來解決。

您可以使用自動 JUnit 斷言來驗證生成的 SQL 語句的預期計數。 db-util項目已經提供了這個功能,它是開源的,依賴項在 Maven Central 上可用。

假設我們有一個與 Contact 有多對一關系的類制造商。

我們通過確保初始查詢獲取在適當初始化狀態下加載我們需要的對象所需的所有數據來解決這個問題。 一種方法是使用 HQL 獲取連接。 我們使用 HQL

"from Manufacturer manufacturer join fetch manufacturer.contact contact"

使用 fetch 語句。 這導致內部聯接:

select MANUFACTURER.id from manufacturer and contact ... from 
MANUFACTURER inner join CONTACT on MANUFACTURER.CONTACT_ID=CONTACT.id

使用 Criteria 查詢,我們可以獲得相同的結果

Criteria criteria = session.createCriteria(Manufacturer.class);
criteria.setFetchMode("contact", FetchMode.EAGER);

它創建了 SQL:

select MANUFACTURER.id from MANUFACTURER left outer join CONTACT on 
MANUFACTURER.CONTACT_ID=CONTACT.id where 1=1

在這兩種情況下,我們的查詢都會返回已初始化聯系人的制造商對象列表。 只需運行一個查詢即可返回所需的所有聯系人和制造商信息

有關更多信息,請訪問問題和解決方案的鏈接。

Hibernate 中1 + N 的本機解決方案稱為:

20.1.5. 使用批量獲取

使用批量獲取,如果訪問了一個代理,Hibernate 可以加載多個未初始化的代理。 批量抓取是對惰性選擇抓取策略的優化。 我們可以通過兩種方式配置批量獲取:1) 類級別和 2) 集合級別...

檢查這些問答:

使用注釋,我們可以這樣做:

A class

@Entity
@BatchSize(size=25)
@Table(...
public class MyEntity implements java.io.Serializable {...

collection級:

@OneToMany(fetch = FetchType.LAZY...)
@BatchSize(size=25)
public Set<MyEntity> getMyColl() 

延遲加載和批量獲取一起代表優化,即:

  • 不需要我們查詢任何明確的取
  • 將應用於加載根實體后(懶惰地)觸摸的任何數量的引用(而顯式提取效果僅在查詢中命名)
  • 將通過集合解決問題 1 + N (因為只有一個集合可以通過根查詢獲取)而無需進一步處理獲取 DISTINCT 根值(檢查: Criteria.DISTINCT_ROOT_ENTITY vs Projections.distinct

你甚至可以讓它工作而無需在任何地方添加@BatchSize注釋,只需將屬性hibernate.default_batch_fetch_size設置為所需的值即可全局啟用批量提取。 有關詳細信息,請參閱Hibernate 文檔

當您使用它時,您可能還想更改BatchFetchStyle ,因為默認值 ( LEGACY ) 很可能不是您想要的。 因此,全局啟用批量獲取的完整配置如下所示:

hibernate.batch_fetch_style=PADDED
hibernate.default_batch_fetch_size=25

此外,我很驚訝提議的解決方案之一涉及連接提取。 Join-fetching 很少是可取的,因為它會導致每個結果行傳輸更多數據,即使依賴實體已經加載到 L1 或 L2 緩存中。 因此,我建議通過設置來完全禁用它

hibernate.max_fetch_depth=0

這是一個常見問題,所以我創建了文章消除 Spring Hibernate N+1 查詢來詳細說明解決方案

為了幫助您檢測應用程序中的所有 N+1 查詢並避免添加更多查詢,我創建了自動檢測 Hibernate N+1 查詢的庫spring-hibernate-query-utils

下面是一些代碼來解釋如何將它添加到您的應用程序中:

  • 將庫添加到您的依賴項
<dependency>
    <groupId>com.yannbriancon</groupId>
    <artifactId>spring-hibernate-query-utils</artifactId>
    <version>1.0.0</version>
</dependency>
  • 在您的應用程序屬性中配置它以返回異常,默認為錯誤日志
hibernate.query.interceptor.error-level=EXCEPTION

如果您使用Spring Data JPA來實現您的存儲庫,您可以在JPA關聯中指定延遲獲取:

@Entity
@Table(name = "film", schema = "public")
public class Film implements Serializable {

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "language_id", nullable = false)
  private Language language;

  @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "film")
  private Set<FilmActor> filmActors;
...
}

@Entity
@Table(name = "film_actor", schema = "public")
public class FilmActor implements Serializable {

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "film_id", nullable = false, insertable = false, updatable = false)
  private Film film;

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "actor_id", nullable = false, insertable = false, updatable = false)
  private Actor actor;
...
}

@Entity
@Table(name = "actor", schema = "public")
public class Actor implements Serializable {

  @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "actor")
  private Set<FilmActor> filmActors;
...
}

並將@EntityGraph添加到基於Spring Data JPA的存儲庫中:

@Repository
public interface FilmDao extends JpaRepository<Film, Integer> {

  @EntityGraph(
    type = EntityGraphType.FETCH,
    attributePaths = {
      "language",
      "filmActors",
      "filmActors.actor"
    }
  )
  Page<Film> findAll(Pageable pageable);
...
}

我在https://tech.asimio.net/2020/11/06/Preventing-N-plus-1-select-problem-using-Spring-Data-JPA-EntityGraph.html 上的博客文章可幫助您防止 N+1使用Spring Data JPA@EntityGraph選擇問題。

以下是一些可以幫助您解決 N+1 問題的代碼片段。

與經理和客戶實體的一對多關系。

客戶端 JPA 存儲庫 -

public interface ClientDetailsRepository extends JpaRepository<ClientEntity, Long> {
    @Query("FROM clientMaster c join fetch c.manager m where m.managerId= :managerId")
    List<ClientEntity> findClientByManagerId(String managerId);
}

經理實體 -

@Entity(name = "portfolioManager")
@Table(name = "portfolio_manager")
public class ManagerEntity implements Serializable {

      // some fields

@OneToMany(fetch = FetchType.LAZY, mappedBy = "manager")
protected List<ClientEntity> clients = new ArrayList<>();

     // Getter & Setter 

}

客戶實體 -

@Entity(name = "clientMaster")
@Table(name = "clientMaster")
public class ClientEntity implements Serializable {

    // some fields

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "manager_id", insertable = false, updatable = false)
    protected ManagerEntity manager;

    // Getter & Setter 

 }

最后,生成輸出 -

Hibernate: select cliententi0_.client_id as client_id1_0_0_, cliententi0_.manager_id as manager_id2_0_0_, managerent1_.manager_id as manager_id1_2_1_, cliententi0_.created_by as created_by7_0_0_, cliententi0_.created_date as created_date3_0_0_, cliententi0_.client_name as client_name4_0_0_, cliententi0_.sector_name as sector_name5_0_0_, cliententi0_.updated_by as updated_by8_0_0_, cliententi0_.updated_date as updated_date6_0_0_, managerent1_.manager_name as manager_name2_2_1_ from client_master cliententi0_, portfolio_manager managerent1_ where cliententi0_.manager_id=managerent1_.manager_id and managerent1_.manager_id=?```

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM