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

Vip*_*wal 31 java orm design-patterns hibernate jpa

我知道N + 1问题是执行一个查询以获取N个记录和N个查询以获取某些关系记录.

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

kar*_*sen 31

假设我们有一个与Contact有多对一关系的类制造商.

我们通过确保初始查询获取在适当初始化状态下加载所需对象所需的所有数据来解决此问题.一种方法是使用HQL提取连接.我们使用HQL

"from Manufacturer manufacturer join fetch manufacturer.contact contact"
Run Code Online (Sandbox Code Playgroud)

使用fetch语句.这导致内部联接:

select MANUFACTURER.id from manufacturer and contact ... from 
MANUFACTURER inner join CONTACT on MANUFACTURER.CONTACT_ID=CONTACT.id
Run Code Online (Sandbox Code Playgroud)

使用Criteria查询我们可以得到相同的结果

Criteria criteria = session.createCriteria(Manufacturer.class);
criteria.setFetchMode("contact", FetchMode.EAGER);
Run Code Online (Sandbox Code Playgroud)

这会创建SQL:

select MANUFACTURER.id from MANUFACTURER left outer join CONTACT on 
MANUFACTURER.CONTACT_ID=CONTACT.id where 1=1
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,我们的查询返回初始化联系人的制造商对象列表.只需运行一个查询即可返回所需的所有联系人和制造商信息

有关详细信息,请参阅此问题解决方案的链接


Vla*_*cea 25

问题

当您忘记获取关联然后需要访问它时,会发生N + 1查询问题.

例如,假设我们有以下JPA查询:

List<PostComment> comments = entityManager.createQuery(
    "select pc " +
    "from PostComment pc " +
    "where pc.review = :review", PostComment.class)
.setParameter("review", review)
.getResultList();
Run Code Online (Sandbox Code Playgroud)

现在,如果我们迭代PostComment实体并遍历post关联:

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

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'
Run Code Online (Sandbox Code Playgroud)

这就是生成N + 1查询问题的方法.

因为post在获取PostComment实体时没有初始化关联,所以Hibernate必须Post使用辅助查询来获取实体,并且对于N个PostComment实体,将执行N个更多查询(因此N + 1查询问题).

修复

解决此问题需要做的第一件事是添加适当的SQL日志记录和监视.如果没有日志记录,您在开发某个功能时就不会注意到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();
Run Code Online (Sandbox Code Playgroud)

如果需要获取多个子关联,最好在初始查询中获取一个集合,而在第二个SQL查询中获取第二个集合.

这个问题最好被集成测试捕获.您可以使用自动JUnit断言来验证生成的SQL语句的预期计数.在DB-util的项目已经提供了这个功能,并且它是开源的,而且依赖性可Maven的中央.


Rad*_*ler 14

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

20.1.5.使用批量提取

使用批量提取,如果访问一个代理,Hibernate可以加载几个未初始化的代理.批量提取是延迟选择提取策略的优化.我们可以通过两种方式配置批量提取:1)类级别和2)集合级别...

查看这些问答:

使用注释我们可以这样做:

一个class级别:

@Entity
@BatchSize(size=25)
@Table(...
public class MyEntity implements java.io.Serializable {...
Run Code Online (Sandbox Code Playgroud)

一个collection级别:

@OneToMany(fetch = FetchType.LAZY...)
@BatchSize(size=25)
public Set<MyEntity> getMyColl() 
Run Code Online (Sandbox Code Playgroud)

延迟加载和批量提取一起表示优化,其中:

  • 要求任何明确的抓取我们查询
  • 将被应用于加载根实体后(懒惰)触摸的任何数量的引用(显式提取效果仅在查询中命名)
  • 将使用集合 解决问题1 + N (因为只能使用根查询获取一个集合)而无需进一步处理获取DISTINCT根值(检查:Criteria.DISTINCT_ROOT_ENTITY vs Projections.distinct)

  • 我认为批量获取或延迟加载只是延迟查询,并没有真正避免多个查询。这并不能解决问题本身。这只是减少其影响的一种方法。 (2认同)
  • +1.延迟加载与批量提取,并且只有在真正需要时,查询中的"连接提取"是处理n + 1选择问题的最直接和最标准的方法. (2认同)
  • 批量获取无法避免n + 1个查询,而是将n个查询的数量除以批量大小。因此,适当的问题是联接获取。此外,批处理获取是在实体级别定义的,因此,如果多个查询使用同一实体,则它们将使用相同的批处理大小,这可能会出现问题(想象一下在批处理和GUI中使用的同一实体) (2认同)

oot*_*ero 10

如果您使用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;
...
}
Run Code Online (Sandbox Code Playgroud)

并添加@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);
...
}
Run Code Online (Sandbox Code Playgroud)

我的博客文章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


phi*_*ppn 6

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

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

hibernate.batch_fetch_style=PADDED
hibernate.default_batch_fetch_size=25
Run Code Online (Sandbox Code Playgroud)

另外,我很惊讶所提出的解决方案之一涉及连接获取。连接获取很少是可取的,因为它会导致每个结果行传输更多数据,即使依赖实体已经加载到 L1 或 L2 缓存中也是如此。因此我建议通过设置来完全禁用它

hibernate.max_fetch_depth=0
Run Code Online (Sandbox Code Playgroud)


Ybr*_*bri 6

这是一个常见问题,因此我创建了文章《消除 Spring Hibernate N+1 查询》来详细介绍解决方案

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

以下代码解释了如何将其添加到您的应用程序中:

  • 将库添加到您的依赖项中
<dependency>
    <groupId>com.yannbriancon</groupId>
    <artifactId>spring-hibernate-query-utils</artifactId>
    <version>1.0.3</version>
</dependency>
Run Code Online (Sandbox Code Playgroud)
  • 在应用程序属性中配置它以返回异常,默认为错误日志
hibernate.query.interceptor.error-level=EXCEPTION
Run Code Online (Sandbox Code Playgroud)