Skip to content

Spring Data JPA与N+1问题

更新: 1/12/2026 字数: 0 字 时长: 0 分钟

Spring Data JPA是Spring旗下的Spring Data项目下针对SQL的ORM,其名字中的JPA为Java Persistent API(即Java持久化API),是Java为了统一各大ORM厂商的接口而定义的一套规范,至于JPA的具体实现则交由各大ORM厂商自行实现,目前Spring Data JPA默认采用的是Hibernate作为JPA实现

N+1问题是什么

所谓N+1问题,是在这样一种语境下出现的:

存在表A B,其中表A中的实体与表B中的实体是一对多的关系,在这种情况下,如果我们使用JPA去查询N条表A中的数据,会连带进行N次查询,去查询每一条表A的实体下关联的表B的实体的数量,进而总计为N次对表B的查询,与一次对表A的查询,即N+1问题

解决方案

目前JPA会推荐我们使用@ManyToOne(fetch = FetchType.LAZY)的方式去指定关联关系

kotlin
@Entity  
@Table  
class Course(  
    @Id  
    @SequenceGenerator(  
        name="course_sequence",  
        sequenceName = "course_sequence",  
        allocationSize = 1  
    )  
    @GeneratedValue(  
        strategy = GenerationType.SEQUENCE,  
        generator = "course_sequence"  
    )  
    var courseId: Long?=null,  
    var title: String,  
    var credit: String,  
    @OneToOne(mappedBy = "course")  
    var courseMaterial: CourseMaterial,  
    @ManyToOne(fetch = FetchType.LAZY)  
    @JoinColumn(  
        name="teacher_id",  
        foreignKey = ForeignKey(ConstraintMode.NO_CONSTRAINT)  
    )  
    var teacher: Teacher  
){  
  
    override fun toString(): String {  
        return "Course(courseId=$courseId, title='$title', credit='$credit', courseMaterial=$courseMaterial, teacher=$teacher)"  
    }  
}

在这种情况下,我们将N+1问题变成了可控的情况

懒加载后带来的问题

虽然解决了N+1问题,但是随之而来的会有更多的问题

我们看上面对course代码中toString方法的定义,可能会感觉没有什么问题,可是我们在Junit下使用findById查询course数据并将得到的实体类打印出来,很快就会发现出现了报错,这是因为我们在调用toString方法的时候,由于其中的关联实体使用的是懒加载,因此查询完course后就会关闭原本的查询session,而打印时在获取关联实体时会因为session已经关闭而获取不到关联的实体类

因此我们需要对toString方法进行改造

kotlin
@Entity  
@Table  
class Course(  
    @Id  
    @SequenceGenerator(  
        name="course_sequence",  
        sequenceName = "course_sequence",  
        allocationSize = 1  
    )  
    @GeneratedValue(  
        strategy = GenerationType.SEQUENCE,  
        generator = "course_sequence"  
    )  
    var courseId: Long?=null,  
    var title: String,  
    var credit: String,  
    @OneToOne(mappedBy = "course")  
    var courseMaterial: CourseMaterial,  
    @ManyToOne(fetch = FetchType.LAZY)  
    @JoinColumn(  
        name="teacher_id",  
        foreignKey = ForeignKey(ConstraintMode.NO_CONSTRAINT)  
    )  
    var teacher: Teacher  
){  
  
    override fun toString(): String {  
        return "Course(courseId=$courseId, title='$title', credit='$credit')"  
    }  
}

JPA推荐我们使用的方式是在toString方法中只打印实体自身的内容

这里值得一提的是,如果你想要原本的Session可以在进行完findById后不关闭,可以在方法上加上@Transactional,这样就会让这一个方法公用一个session,也就不会导致session的提前关闭,进而完成查询

关联查询的方法

实际上,虽然我们通过懒加载解决了N+1问题,但关联查询是不可避免的,那么我们该如何进行关联查询呢?

一次查询所有关联数据

这种查询适合原本的N+1问题相似的,也就是我们查询A表的数据,结果要求查出A表实体下的所有相关联的B表实体,这种情况下我们应该使用JPA为我们提供的@EntityGraph注解

kotlin
@Repository
interface CourseRepository: JpaRepository<Course, Long>{

    @EntityGraph(attributePaths = ["teacher","courseMaterial"])
    override fun findAll(): MutableList<Course>

}

@EntityGraph可以加在实体类亦或是仓储的接口上,一般为了灵活性我们会直接加在仓储的接口上,@EntityGraph会将原本的N+1语句转换为left join的方法进行查询,进而使用一条语句完成了查询

一次查询指定数量的关联数据

这里我们首先需要明白一点,查询A表的一个实体,同时关联的查询B表中指定数量的关联实体,这件事情本身,如果希望通过一次SQL完成的话就属于是复杂查询的范畴了

针对复杂查询,我们一般会根据QPS做出抉择:

  • 当QPS较低时,我们其实应该照顾代码的可维护性,因此,最优解应该是执行两次简单查询,进而拼凑出我们需要的
  • 当QPS较高时,我们需要更高的查询效率,这时JPA其实已经有点不太够用了,因为JPA本身对于这种情况下对我们的希望就是使用原生的SQL,因此我们可以使用@Query的方式定义原生SQL进行查询
本站访客数 人次      本站总访问量