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

时间:2015-09-08 09:24:29

标签: java hibernate jpa design-patterns orm

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

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

7 个答案:

答案 0 :(得分:26)

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

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

有关详细信息,请参阅problemsolution

的链接

答案 1 :(得分:18)

问题

当您忘记获取关联然后需要访问它时,会发生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查询问题的生成方式。

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

修复

解决此问题需要做的第一件事是添加proper SQL logging and monitoring。如果没有日志记录,您在开发某个功能时就不会注意到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查询。

此问题最好被集成测试捕获。您可以使用automatic JUnit assert to validate the expected count of generated SQL statementsdb-util project已经提供了这个功能,它是开源的,并且依赖在Maven Central上可用。

答案 2 :(得分:13)

Hibernate中 1 + N 的原生解决方案称为:

20.1.5. Using batch fetching

  

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

检查这些Q&amp;答:

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

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(因为只能通过root查询获取 )而无需进一步处理获取DISTINCT根值(选中:Criteria.DISTINCT_ROOT_ENTITY vs Projections.distinct

答案 3 :(得分:4)

这是一个常见问题,因此我创建了文章Eliminate Spring Hibernate N+1 Queries来详细说明解决方案

为帮助您检测应用程序中的所有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>
  • 在应用程序属性中配置它以返回异常,默认为错误日志
hibernate.query.interceptor.error-level=EXCEPTION

答案 4 :(得分:2)

您甚至可以使其工作而不必在各处添加@BatchSize批注,只需将属性hibernate.default_batch_fetch_size设置为所需值即可全局启用批量提取。有关详细信息,请参见Hibernate docs

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

hibernate.batch_fetch_style=PADDED
hibernate.default_batch_fetch_size=25

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

hibernate.max_fetch_depth=0

答案 5 :(得分:0)

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

答案 6 :(得分:0)

如果您使用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的博客文章可帮助您防止使用Spring Data JPA@EntityGraph进行N + 1选择问题。