JPA 和 Hibernate 中的 N+1 问题的解决方案是啥?
Posted
技术标签:
【中文标题】JPA 和 Hibernate 中的 N+1 问题的解决方案是啥?【英文标题】:What is the solution for the N+1 issue in JPA and Hibernate?JPA 和 Hibernate 中的 N+1 问题的解决方案是什么? 【发布时间】:2015-12-03 21:55:52 【问题描述】:我知道 N+1 问题是执行一个查询以获取 N 条记录,执行 N 个查询以获取一些关系记录。
但是在 Hibernate 中如何避免呢?
【问题讨论】:
使用延迟加载:***.com/q/2192242 @Tunaki:错了。无论您是急切加载还是延迟加载,都会说明选择何时执行。它根本无法避免 N+1 问题。 @BetaRide,也许我误解了你的评论,但尽管你投了七票,但听起来完全错误。 Eager fetching 确实避免了 N+1 问题。请参阅blog post by a JPA expert 的第 3 点。 【参考方案1】:问题
当您忘记获取关联然后您需要访问它时,会发生 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 上可用。
【讨论】:
【参考方案2】:假设我们有一个类 Manufacturer 与 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
在这两种情况下,我们的查询都会返回带有初始化联系人的制造商对象列表。只需运行一次查询即可返回所需的所有联系人和制造商信息
更多信息请点击problem and the solution链接。
【讨论】:
如果我在单个事务中更新同一实体的一组对象,orm 层会发布多个更新,但我希望一个更新语句起作用,我该怎么办。那可行吗?怎么样?【参考方案3】:Hibernate 中 1 + N 的原生解决方案,称为:
20.1.5. Using batch fetching
使用批量获取,如果访问一个代理,Hibernate 可以加载多个未初始化的代理。 批量抓取是对惰性选择抓取策略的优化。我们可以通过两种方式配置批量抓取:1) 类级别和 2) 集合级别...
查看这些问答:
@BatchSize but many round trip in @ManyToOne case Avoiding n+1 eager fetching of child collection element association使用注解我们可以这样做:
一个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()
延迟加载和批量获取一起代表优化,其中:
不需要在我们的查询中要求任何显式提取 将应用于任意数量的引用,这些引用在根实体加载后(懒惰地)被触摸(而显式获取仅影响查询中命名的那些) 将使用 collections 解决问题 1 + N(因为 只有一个集合 可以通过根查询获取) 无需进一步处理获取 DISTINCT 根值 (检查:Criteria.DISTINCT_ROOT_ENTITY vs Projections.distinct)【讨论】:
我认为批量获取或延迟加载只是延迟了查询,并没有真正避免多个查询。这并不能解决问题。这只是减少影响的一种方法。 事实上,我在每个类和每个集合上都使用了这个设置。每一个。每当我加载某个实体的列表并触摸它的引用或集合时......只有一个(取决于批量大小)对所有实体(1 + 1)执行 SELECT。 这是 1 + N 的真正内置解决方案。 更重要的是,我们不必更改查询(在某些引用或集合上使用 Fetch.mode)...我们可以只查询 root实体...稍后(懒惰地)很少 SELECT 我们全部加载...希望更清楚一点;) +1。使用批量获取进行延迟加载,并且仅在真正需要时,查询中的join fetch
是处理 n+1 选择问题的最直接和标准的方法。
批量获取不会避免 n+1 查询,而是将 n 查询的数量除以批量大小。所以适当的问题是连接提取。此外,批量获取是在实体级别定义的,因此如果多个查询使用相同的实体,它们将使用相同的批量大小并且可能会出现问题(想象在批量和 GUI 中使用相同的实体)【参考方案4】:
您甚至可以让它工作而无需在任何地方添加@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】:这是一个常见问题,所以我创建了文章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
【讨论】:
欢迎来到 Stack Overflow!虽然自我推销不是问题,但如果有帮助,请考虑添加一些与解决实际问题相关的代码/解释,使其成为有价值的答案,或者在您回答足够多的问题后考虑使用 cmets :) 感谢您的建议 @aksh1618 我添加了一些代码来说明它与问题的关系。【参考方案6】:如果您使用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 选择问题。
【讨论】:
【参考方案7】:这里有一些可以帮助您解决 N+1 问题的 sn-p 代码。
与经理和客户实体的一对多关系。
客户端 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=?```
【讨论】:
以上是关于JPA 和 Hibernate 中的 N+1 问题的解决方案是啥?的主要内容,如果未能解决你的问题,请参考以下文章
Spring JPA/Hibernate Repository findAll 在 Kotlin 中默认执行 N+1 个请求而不是 JOIN
JPA 和 Hibernate 中的 persist() 和 merge() 有啥区别?