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

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

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

假设我们有一个与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)集合级别…

查看这些问答:

  • @BatchSize但在@ManyToOne案例中有很多往返
  • 避免n + 1渴望获取子集合元素关联

有了注释,我们可以这样做:

class

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

collection级别:

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

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

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

问题

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

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

 List 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日志记录和监视 。 如果没有日志记录,在开发某个function时,您将不会注意到N + 1查询问题。

其次,为了解决这个问题,你可以加入FETCH导致这个问题的关系:

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

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

这个问题最好被集成测试捕获。 您可以使用自动JUnit断言来validation生成的SQL语句的预期计数 。 db-unit项目已经提供了这个function,它是开源的,并且可以在Maven Central上使用依赖项。