首页 > 解决方案 > 非主键的 JPA Hibernate FetchType.EAGER 和 FetchMode.JOIN

问题描述

我遇到了一个非常有趣的场景。我知道 n+1 问题以及 FetchType.EAGER 和 FetchMode.JOIN。我有一个父实体学校,它有 2 个@OneToMany 子实体,即学生和教师。我需要全部 3 个,所以使用 FetchType.EAGER 和 FetchMode.JOIN。

学校实体

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Fetch;
import org.hibernate.annotations.FetchMode;
import javax.persistence.*;
import java.util.Set;
@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
public class School {
    @Id
    @GeneratedValue(generator = "sequence", strategy = GenerationType.IDENTITY)
    @SequenceGenerator(name = "sequence", allocationSize = 10)
    int schoolId;
    String schoolName;
    float schoolRating;
    @OneToMany(mappedBy = "school", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
    @Fetch(FetchMode.JOIN)
    private Set<Teacher> teachers;
    @OneToMany(mappedBy = "school", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER)
    @Fetch(FetchMode.JOIN)
    private Set<Student> students;
 }

学生实体

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.util.Date;
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Student {
    @Id
    @GeneratedValue(generator = "sequence", strategy = GenerationType.IDENTITY)
    @SequenceGenerator(name = "sequence", allocationSize = 10)
    public int studentId;
    public byte studentByte;
    public Date date;
    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "schoolId", referencedColumnName = "schoolId")
    private School school;
}

教师实体

import lombok.AllArgsConstructor;
import lombok.NoArgsConstructor;    
import javax.persistence.*;
import java.util.Date;
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Teacher {
    @Id
    @GeneratedValue(generator = "sequence", strategy = GenerationType.IDENTITY)
    @SequenceGenerator(name = "sequence", allocationSize = 10)
    public int teacherId;
    public byte teacherByte;
    public Date date;
    @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    @JoinColumn(name = "schoolId", referencedColumnName = "schoolId")
    private School school;
}

学校回购

@Repository
public interface SchoolRepository extends JpaRepository<School, Integer> {
List<School>findBySchoolName(String schoolName);
}

如果我通过 findById 方法获取 School 对象,即表的主键。

Optional<School> schoolById = schoolRepository.findById(1);

生成的 SQL 是学校、学生和教师实体的连接。

select school0_.schoolId as schoolid1_0_0_, school0_.schoolName as schoolna2_0_0_, school0_.schoolRating as schoolra3_0_0_, students1_.schoolId as schoolid4_1_1_, students1_.studentId as studenti1_1_1_, students1_.studentId as studenti1_1_2_, students1_.date as date2_1_2_, students1_.schoolId as schoolid4_1_2_, students1_.studentByte as studentb3_1_2_, teachers2_.schoolId as schoolid4_2_3_, teachers2_.teacherId as teacheri1_2_3_, teachers2_.teacherId as teacheri1_2_4_, teachers2_.date as date2_2_4_, teachers2_.schoolId as schoolid4_2_4_, teachers2_.teacherByte as teacherb3_2_4_ from School school0_ left outer join Student students1_ on school0_.schoolId=students1_.schoolId left outer join Teacher teachers2_ on school0_.schoolId=teachers2_.schoolId where school0_.schoolId=?

但是如果我找到一所学校有一些其他变量不是主键。

List<School> schoolByName = schoolRepository.findBySchoolName("school1");

生成的 SQL 在 DB 上是 3 个不同的命中。

Hibernate: select school0_.schoolId as schoolid1_0_, school0_.schoolName as schoolna2_0_, school0_.schoolRating as schoolra3_0_ from School school0_ where school0_.schoolName=?
Hibernate: select teachers0_.schoolId as schoolid4_2_0_, teachers0_.teacherId as teacheri1_2_0_, teachers0_.teacherId as teacheri1_2_1_, teachers0_.date as date2_2_1_, teachers0_.schoolId as schoolid4_2_1_, teachers0_.teacherByte as teacherb3_2_1_ from Teacher teachers0_ where teachers0_.schoolId=?
Hibernate: select students0_.schoolId as schoolid4_1_0_, students0_.studentId as studenti1_1_0_, students0_.studentId as studenti1_1_1_, students0_.date as date2_1_1_, students0_.schoolId as schoolid4_1_1_, students0_.studentByte as studentb3_1_1_ from Student students0_ where students0_.schoolId=?

我意识到加入只有在我们通过 id 即主键获取时才有效,但我没有 School 的主键。我有学校的名称,它是唯一的和索引的,也需要学生实体和教师实体。有没有办法让他们都在休眠中使用加入。我知道如果学生和老师的记录更多,那将是性能下降,但就我而言,最多只有 3-4 条记录。这就是为什么我想加入他们。

标签: javahibernatejpaspring-data-jpaspring-data

解决方案


  • 不建议将 Entity 的多个关联集合字段映射到FetchMode.JOIN. 这是为了避免Cartesian product issue。我很惊讶即使您选择了它,它也执行了 sql joinId

  • 当您获取除其Id字段之外的 School 时,hibernate 不知道您将获取多少个 Schools,因此如果它执行的是 join fetch 而不是单独选择,它将以Cartesian product issue

  • 假设你有 10 所学校,每所学校有 20 名教师和 400 名学生。如果 hibernate 进行了连接,它将必须从 db中带来80,000( ) 记录。10*20*400但由于它是单独选择,它会带来4,210( 10 + 200 + 4000) 记录。即使在通过Id它选择的情况下也是420记录与8000记录

简答

即使您找到了一种方法,也不要使用连接检索父实体及其多个关联集合,因为性能会比多选差。

更新:

  • 如果您确定学校名称是唯一的并且每所学校只有几位老师并且学生人数很少,您可以执行以下操作:(当前您的findBySchoolName退货List<School>,您可以将其更改为返回可选学校)
@Repository
public interface SchoolRepository extends JpaRepository<School, Integer> {
    
    @Query("SELECT s from School s left join fetch s.teachers " +
            "left join fetch s.students where s.schoolName = :name")
    Optional<School> findBySchoolName(String name);
}

推荐阅读