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

2022-08-31 15:47:51

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

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


答案 1

问题

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

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

List<PostComment> comments = entityManager.createQuery("""
    select pc
    from PostComment pc
    where pc.review = :review
    """, PostComment.class)
.setParameter("review", review)
.getResultList();

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

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

休眠将生成以下 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 查询问题的方式。

由于在获取实体时未初始化关联,因此 Hibernate 必须使用辅助查询获取实体,对于 N 个实体,将执行 N 个以上的查询(因此存在 N+1 查询问题)。postPostCommentPostPostComment

修复

要解决此问题,您需要做的第一件事是添加[适当的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 上可用。


答案 2

假设我们有一个与联系人具有多对一关系的类制造商。

我们通过确保初始查询获取在适当初始化状态下加载所需对象所需的所有数据来解决此问题。执行此操作的一种方法是使用 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 = 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

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

有关更多信息,此处是指向问题和解决方案的链接。


推荐