Spring JPA/Hibernate Repository findAll 在 Kotlin 中默认执行 N+1 个请求而不是 JOIN
Posted
技术标签:
【中文标题】Spring JPA/Hibernate Repository findAll 在 Kotlin 中默认执行 N+1 个请求而不是 JOIN【英文标题】:Spring JPA/Hibernate Repository findAll is doing N+1 requests instead of a JOIN by default in Kotlin 【发布时间】:2021-04-18 17:20:15 【问题描述】:我正在使用 Kotlin 处理 Spring JPA/Hibernate 应用程序,我想查找实体中的所有元素。
该实体有一个具有@ManyToOne 关系的外键。我想通过 JOIN 查询获取所有元素及其关联值,从而避免 N+1 问题。
一件事是外键与主键无关,而是与实体中的另一个唯一字段 (UUID) 相关。
我能够使用 JOIN 进行该查询,创建一个使用 JOIN FETCH 的自定义查询,但我的意思是避免创建这些查询并在所有 findAlls 中默认创建这些 JOINS。
这可能吗,还是我必须手动在 JPQL 中进行查询以强制 JOIN FETCH?
示例代码如下:
@Entity
data class A
@Id
val id: Long,
@Column
val uuid: UUID,
@Column
val name: String
@Entity
data class B
@Id
val id: Long,
...
@Fetch(FetchMode.JOIN)
@ManyToOne
@JoinColumn(name = "a_uuid", referencedColumnName = "uuid", insertable = false, updatable = false)
val a: A
@Repository
interface Repo<B> : CrudRepository<B, Long>
...
repo.findAll() // <-- This triggers N+1 queries instead of making a JOIN
...
【问题讨论】:
如果您使用@BatchSize(size = 1000) 或您认为合适的任何大小来注释您的实体A,会发生什么?这会改变行为吗?我还建议将 fetch 更改为惰性 @ManyToOne(fetch = javax.persistence.FetchType.LAZY)。这与引用实体上的 BatchSize 配对将产生 2 个查询(或更准确地说 -> 1 + N/1000 个查询) 【参考方案1】:据我所知,获取模式仅适用于EntityManager.find
相关查询或在执行延迟加载时,但在执行 HQL 查询时不适用,这是幕后发生的事情。如果您希望它被加入获取,您将不得不使用实体图,这在 IMO 中也更好,因为您可以在每个使用站点而不是全局定义它。
【讨论】:
【参考方案2】:我不知道如何准确配置您的要求,但以下建议可能值得考虑...
改变
@Fetch(FetchMode.JOIN)
@ManyToOne
@JoinColumn(name = "a_uuid", referencedColumnName = "uuid", insertable = false, updatable = false)
val a: A
到
@ManyToOne(fetch = javax.persistence.FetchType.LAZY)
@JoinColumn(name = "a_uuid", referencedColumnName = "uuid", insertable = false, updatable = false)
val a: A
然后在您的实体 A 上,将注释添加到类中
@BatchSize(size = 1000)
或您认为合适的任何批量大小。
如果您的结果少于 1000 个,这通常会在 2 个查询中为您提供结果。它将为 A 加载代理,而不是加入 A,但是当第一次访问 A 时,它将填充 BATCH_SIZE 个实体的代理。
它减少了来自
的查询数量N + 1
到
1 + round_up(N / BATCH_SIZE)
【讨论】:
【参考方案3】:findAll
实现将始终首先加载b
,然后解析它的依赖项检查注释。如果您想避免 N+1 问题,您可以在 JPQL 查询中添加 @Query 注释:
...
@Query("select b from TableB b left join fetch b.a")
repo.findAll()
...
【讨论】:
【参考方案4】:您的另一个选择是使用EntityGraph
。它允许通过对我们要检索的相关持久性字段进行分组来定义模板,并允许我们在运行时选择图形类型。
这是通过修改您的代码生成的示例代码。
@Entity
data class A (
@Id
val id: Long,
@Column
val uuid: UUID,
@Column
val name: String
) : Serializable
@NamedEntityGraph(
name = "b_with_all_associations",
includeAllAttributes = true
)
@Entity
data class B (
@Id
val id: Long,
@ManyToOne
@JoinColumn(name = "a_uuid", referencedColumnName = "uuid")
val a: A
)
@Repository
interface ARepo: CrudRepository<A, Long>
@Repository
interface BRepo: CrudRepository<B, Long>
@EntityGraph(value = "b_with_all_associations", type = EntityGraph.EntityGraphType.FETCH)
override fun findAll(): List<B>
@Service
class Main(
private val aRepo: ARepo,
private val bRepo: BRepo
) : CommandLineRunner
override fun run(vararg args: String?)
(1..3L).forEach
val a = aRepo.save(A(id = it, uuid = UUID.randomUUID(), name = "Name-$it"))
bRepo.save(B(id = it + 100, a = a))
println("===============================================")
println("===============================================")
println("===============================================")
println("===============================================")
bRepo.findAll()
在B
实体上,定义了一个名为“b_with_all_associations”的实体图,并应用于LOAD
类型的B实体的repository的findAll
方法。
这些东西将通过使用join
获取来防止您的 N+1 问题。
这是bRepo.findAll()
的 SQL 日志。
select
b0_.id as id1_1_0_,
a1_.id as id1_0_1_,
b0_.a_uuid as a_uuid2_1_0_,
a1_.name as name2_0_1_,
a1_.uuid as uuid3_0_1_
from
b b0_
left outer join
a a1_
on b0_.a_uuid=a1_.uuid
ps1。由于这个issue,我不建议使用与非pk 的多对一关系。它迫使我们将java.io.Serializable
用于“一个”实体。
ps2。当您想用Join
解决 N+1 问题时,EntityGraph
可以很好地回答您的问题。但我会推荐更好的解决方案:尝试使用延迟加载来解决它。
ps3。对 Hibernate 使用非 pk 关联不是一个好主意。我真的同意this comment。我认为这是一个尚未解决的错误。它打破了hibernate的延迟加载机制。
【讨论】:
以上是关于Spring JPA/Hibernate Repository findAll 在 Kotlin 中默认执行 N+1 个请求而不是 JOIN的主要内容,如果未能解决你的问题,请参考以下文章
JPA---Spring-data-JPA---Hibernate
Spring+Jersey+JPA+Hibernate+MySQL实现CRUD操作案例
Spring,JPA,Hibernate,Atomikos - 奇怪的启动错误
Spring Boot ManyToMany - *** - JPA,Hibernate