JPA 和 Hibernate 中 N+1 问题的解决方案是什么?

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

我理解 N+1 问题是执行一个查询以获取 N 条记录和执行 N 个查询以获取一些关系记录。

但是在Hibernate中如何避免呢?

假设我们有一个 class 制造商与 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

在这两种情况下,我们的查询 return 都是具有已初始化联系人的制造商对象列表。只需要一个查询就可以运行 return所有需要的联系方式和厂商信息

有关详细信息,请参阅 link 到 problem and the solution

Hibernate 中 1 + N 的本机解决方案称为:

20.1.5. Using batch fetching

Using batch fetching, Hibernate can load several uninitialized proxies if one proxy is accessed. Batch fetching is an optimization of the lazy select fetching strategy. There are two ways we can configure batch fetching: on the 1) class level and the 2) collection level...

查看这些问答:

  • @BatchSize but many round trip in @ManyToOne case
  • Avoiding n+1 eager fetching of child collection element association

有了注解我们可以这样做:

Aclass级:

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

Acollection级:

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

延迟加载和批量抓取一起代表优化,即:

  • 不需要在我们的查询中显式获取
  • 将应用于任意数量的引用,这些引用在加载根实体后被(懒惰地)触摸(而显式获取仅影响查询中命名的那些)
  • 将使用 collections 解决问题 1 + N(因为 only one collection could be fetched with root query) 无需进一步处理获取 DISTINCT 根值 (检查:Criteria.DISTINCT_ROOT_ENTITY vs Projections.distinct

问题

当您忘记获取关联然后需要访问它时,会发生 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 project 已经提供了这个功能,它是开源的,依赖项在 Maven Central 上可用。

您甚至无需在任何地方添加 @BatchSize 注释即可使其正常工作,只需将 属性 hibernate.default_batch_fetch_size 设置为所需的值即可全局启用批量提取。有关详细信息,请参阅 Hibernate docs

当您这样做时,您可能还想更改 BatchFetchStyle,因为默认值 (LEGACY) 很可能不是您想要的。因此,全局启用批量获取的完整配置如下所示:

hibernate.batch_fetch_style=PADDED
hibernate.default_batch_fetch_size=25

此外,令我惊讶的是,提议的解决方案之一涉及联合获取。 Join-fetching 很少是可取的,因为它会导致每个结果行传输更多数据,即使相关实体已经加载到 L1 或 L2 缓存中也是如此。因此我建议通过设置

来完全禁用它
hibernate.max_fetch_depth=0

This is a frequently asked question so I created the article Eliminate Spring Hibernate N+1 Queries to detail the solutions

为了帮助您检测应用程序中的所有 N+1 查询并避免添加更多查询,我创建了库 spring-hibernate-query-utils 来自动检测 Hibernate N+1 查询。

这里有一些代码来解释如何将它添加到您的应用程序中:

  • 将库添加到您的依赖项
<dependency>
    <groupId>com.yannbriancon</groupId>
    <artifactId>spring-hibernate-query-utils</artifactId>
    <version>1.0.0</version>
</dependency>
  • 在您的应用程序属性中将其配置为 return 异常,默认为错误日志
hibernate.query.interceptor.error-level=EXCEPTION

这里有一些代码片段可以帮助您解决 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=?```

如果您使用 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 的博客 post 帮助您使用 Spring Data JPA@EntityGraph.

防止 N+1 select 问题