从零开始 Spring Boot 68:连接实体
图源:简书 (jianshu.com)
在 JPA 中关联实体实际上对应表连接,而表连接可以通过内连接(Inner Join)、外连接(Outer Join)和 Where等方式实现,实际上 JPA 也用这些方式实现对所关联的实体数据的查询和加载。
本文示例使用以下实体类:
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class School {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@NotBlank
@Length(max = 100)
@Column(unique = true)
private String name;
@OneToMany(mappedBy = "school", cascade = CascadeType.ALL)
@Builder.Default
private List<Student> students = new ArrayList<>();
public School addStudent(Student student){
this.students.add(student);
student.setSchool(this);
return this;
}
}
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class Student {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@NotBlank
@Length(max = 45)
private String name;
@Setter
@ManyToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "school_id")
private School school;
@OneToMany(mappedBy = "student",
cascade = CascadeType.ALL)
@Builder.Default
private List<Email> emails = new ArrayList<>();
public Student addEmail(Email email){
this.emails.add(email);
email.setStudent(this);
return this;
}
}
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(uniqueConstraints = @UniqueConstraint(columnNames = {"account", "domain"}))
public class Email {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotNull
@NotBlank
@Length(max = 45)
private String account;
@NotNull
@NotBlank
@Length(max = 45)
private String domain;
@Setter
@ManyToOne
@JoinColumn(name = "student_id")
private Student student;
}
隐式连接
因为在示例中已经创建了实体之间的对应关系,所有如果查询涉及关联的实体,查询的时候 JPA 会隐式地使用join
SQL 进行表连接以获取数据:
var school = session.createQuery("select s.school from Student s where s.name=:name", School.class)
.setParameter("name", "icexmoon")
.getSingleResult();
Assertions.assertEquals("霍格沃茨魔法学校", school.getName());
SQL 日志:
select s2_0.id,s2_0.name from student s1_0 join school s2_0 on s2_0.id=s1_0.school_id where s1_0.name=?
binding parameter [1] as [VARCHAR] - [icexmoon]
显式内连接
当然,也可以在 JPQL 中显式地使用join
语句进行内连接:
List<Student> students = session.createQuery("select s from Student s join Email e on e.student=s where e.domain=:domain", Student.class)
.setParameter("domain", "gmail.com")
.getResultList();
SQL 日志:
select s1_0.id,s1_0.name,s1_0.school_id from student s1_0 join email e1_0 on e1_0.student_id=s1_0.id where e1_0.domain=?
binding parameter [1] as [VARCHAR] - [gmail.com]
在 JPQL 中,join
实际上指的就是inner join
(内连接),所以inner
是可选的:
List<Student> students = session.createQuery("select s from Student s inner join Email e on e.student=s where e.domain=:domain", Student.class)
.setParameter("domain", "gmail.com")
.getResultList();
查询集合
比较特殊的是,如果查询的结果是集合(Collection),getResultList
返回的是将这些集合元素合并后的结果:
List<Email> emails = session.createQuery("select s.emails from Student s where s.name=:name", Email.class)
.setParameter("name", "icexmoon")
.getResultList();
在这个示例中,s.emails
的类型实际上是List
,但返回的结果并不是List<List>
或List<Collection>
,而是List<Email>
,实际上是将多个List
内的元素合并后的结果。
如果需要对返回的集合添加条件,需要使用join
:
List<Email> emails = session.createQuery("select e from Student s" +
" join Email e on e.student=s" +
" where s.name=:name" +
" and e.domain=:domain",
Email.class)
.setParameter("name", "icexmoon")
.setParameter("domain", "qq.com")
.getResultList();
当然也可以用 Java 遍历结果并筛选。
外连接
JPA 只支持左外连接(left join
),不支持right join
:
List<Student> students = session.createQuery("select s from Student s left join Email e" +
" on e.student=s" +
" where e.domain=:domain", Student.class)
.setParameter("domain", "qq.com")
.getResultList();
如果需要使用右外连接,可以调换left join
两边的实体对象位置。
连接多个实体
和 SQL 中可以连接多张表一样,使用join
可以在 JPQL 中连接多个实体:
List<Student> students = session.createQuery("select s from School sc" +
" left join Student s on s.school=sc" +
" left join Email e on e.student=s" +
" where sc.name=:scname" +
" and e.domain=:domain", Student.class)
.setParameter("scname", "霍格沃茨魔法学校")
.setParameter("domain", "qq.com")
.getResultList();
where
当不存在外键关系时,用where
进行关联很有用:
var students = session.createQuery("select s from Student s,Email e" +
" where s=e.student" +
" and e.domain=:domain", Student.class)
.setParameter("domain", "gmail.com")
.getResultList();
特别的,如果用where
连接实体,且没有指定连接条件时,实际上查询结果是“笛卡尔积”:
var students = session.createQuery("select s from Student s,Email e", Student.class)
.getResultList();
实际执行的 SQL 是:
select s1_0.id,s1_0.name,s1_0.school_id from student s1_0,email e1_0
因为是笛卡尔积,这会对两张关联表都进行全表遍历,性能会很差。
因为实体之间有明确的关联关系,所以这里
Student
的emails
属性会被 Hibernate 用额外查询进行关联,所以最后获取到的Student
对象不能体现笛卡尔积的查询结果。
The End,谢谢阅读。
可以从这里获取本文的完整示例代码。
参考资料
- JPA Join Types | Baeldung