繁体   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