红茶的个人站点

  • 首页
  • 专栏
  • 开发工具
  • 其它
  • 隐私政策
Awalon
Talk is cheap,show me the code.
  1. 首页
  2. 专栏
  3. Spring Boot 学习笔记
  4. 正文

从零开始 Spring Boot 68:连接实体

2023年7月10日 919点热度 0人点赞 0条评论

spring boot

图源:简书 (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

本作品采用 知识共享署名 4.0 国际许可协议 进行许可
标签: join jpql spring
最后更新:2023年7月10日

魔芋红茶

加一点PHP,加一点Go,加一点Python......

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

COPYRIGHT © 2021 icexmoon.cn. ALL RIGHTS RESERVED.
本网站由提供CDN加速/云存储服务

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号