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

Spring Boot / JPA / Hibernate,如何根据 Spring 配置文件切换数据库供应商?

spring-data-jpa+hibernate 各种缓存的配置演示