红茶的个人站点

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

从零开始 Spring Boot 61:JPA 中的级联类型

2023年7月5日 1282点热度 0人点赞 0条评论

spring boot

图源:简书 (jianshu.com)

关系型数据库的增删改查操作会因为有关联关系而存在“级联操作”的需要,体现在 JPA 中,就是实体中会定义的级联类型(Cascade Type)。

JPA 中的级联类型由枚举jakarta.persistence.CascadeType表示,包括:

  • ALL

  • PERSIST

  • MERGE

  • REMOVE

  • REFRESH

  • DETACH

这些级联类型对应实体对象的状态转换操作,具体可以参考这篇文章。

ALL包含其他所有的操作。

下面详细说明这些级联类型的用途和影响。

示例

本文将使用以下的示例说明级联操作的影响:

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "student")
public class Student {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
​
    @NotNull
    @NotBlank
    @Length(max = 45)
    @Column(unique = true)
    private String name;
​
    @OneToMany(mappedBy = "student",
            fetch = FetchType.LAZY)
    @Builder.Default
    private List<Email> emails = new ArrayList<>();
​
    public Student addEmail(Email email){
        if (this.emails.contains(email)){
            return this;
        }
        this.emails.add(email);
        email.setStudent(this);
        return this;
    }
}
​
@Accessors(chain = true)
@Setter
@Getter
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "email", uniqueConstraints = @UniqueConstraint(columnNames = {"name", "domain"}))
public class Email {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
​
    @NotNull
    @NotBlank
    @Length(max = 45)
    @EqualsAndHashCode.Include
    private String name;
    @NotBlank
    @NotNull
    @Length(max = 45)
    @EqualsAndHashCode.Include
    private String domain;
​
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "student_id")
    private Student student;
}

这里包含两个实体,一个学生实体实例对应多个电子邮件实体实例。

关于一对多关系的更多介绍可以阅读这篇文章。

PERSIST

如果实体之间的关系不包含任何级联类型,添加一个实体时不会对另一个实体产生任何影响,换言之,添加学生实体实例后,只会插入学生相关表数据,电子邮件表不会有任何数据添加。

如果希望进行“级联添加”,需要使用级联类型CascadeType.PERSIST:

public class Student {
    // ...
    @OneToMany(mappedBy = "student",
               cascade = CascadeType.PERSIST,
               fetch = FetchType.LAZY)
    private List<Email> emails = new ArrayList<>(); 
}

现在,添加新的Student实例时,就会一同添加相关的Email实例到数据库。

测试用例:

students.forEach(s -> {
    session.persist(s);
});

SQL 日志:

insert into student (name) values (?)
binding parameter [1] as [VARCHAR] - [icexmoon]
insert into email (domain,name,student_id) values (?,?,?)
binding parameter [1] as [VARCHAR] - [qq.com]
binding parameter [2] as [VARCHAR] - [icexmoon]
binding parameter [3] as [BIGINT] - [1]
insert into email (domain,name,student_id) values (?,?,?)
binding parameter [1] as [VARCHAR] - [qq.com]
binding parameter [2] as [VARCHAR] - [123]
binding parameter [3] as [BIGINT] - [1]
...

当然,JPA 的 persist API 还包含对持久实体的更新操作,此时同样适用CascadeType.PERSIST级联类型:

var icexmoon = students.stream().filter(s ->        s.getName().equals("icexmoon")).findFirst().get();
long id = icexmoon.getId();
var savedIcexmoon = session.find(Student.class, id);
var icexmoonEmail = savedIcexmoon.getEmails().get(0);
icexmoonEmail.setName("111")
    .setDomain("gmail.com");
session.persist(icexmoonEmail);

SQL 日志:

update email set domain=?,name=?,student_id=? where id=?
binding parameter [1] as [VARCHAR] - [gmail.com]
binding parameter [2] as [VARCHAR] - [111]
binding parameter [3] as [BIGINT] - [1]
binding parameter [4] as [BIGINT] - [1]

当然,使用JPARepository先关的 API 同样是可以的:

studentRepository.saveAndFlush(student);
student.getEmails().get(0)
    .setName("111")
    .setDomain("gmail.com");
studentRepository.saveAndFlush(student);

同样会进行级联插入/更新。

MERGE

CascadeType.MERGE对应 JPA 持久化上下文的merge操作。

示例:

public class Student {
    // ...
    @OneToMany(mappedBy = "student",
            cascade = CascadeType.MERGE,
            fetch = FetchType.LAZY)
    private List<Email> emails = new ArrayList<>();
}

调用示例:

var savedIcexmoon = session.find(Student.class, icexmoon.getId());
session.evict(savedIcexmoon);
savedIcexmoon.getEmails().get(0).setName("111").setDomain("gmail.com");
session.merge(savedIcexmoon);

SQL 日志:

update email set domain=?,name=?,student_id=? where id=?
binding parameter [1] as [VARCHAR] - [gmail.com]
binding parameter [2] as [VARCHAR] - [111]
binding parameter [3] as [BIGINT] - [1]
binding parameter [4] as [BIGINT] - [1]

REMOVE

CascadeType.REMOVE对应持久化上下文的remove操作。

示例:

public class Student {
    // ...
    @OneToMany(mappedBy = "student",
            cascade = {CascadeType.MERGE,
                    CascadeType.PERSIST,
                    CascadeType.REMOVE},
            fetch = FetchType.EAGER)
    private List<Email> emails = new ArrayList<>();
}

调用示例:

var savedIcexmoon = session.find(Student.class, icexmoon.getId());
session.remove(savedIcexmoon);

SQL 日志:

select s1_0.id,s1_0.name,e1_0.student_id,e1_0.id,e1_0.domain,e1_0.name from student s1_0 left join email e1_0 on s1_0.id=e1_0.student_id where s1_0.id=?
binding parameter [1] as [BIGINT] - [1]
delete from email where id=?
binding parameter [1] as [BIGINT] - [1]
delete from email where id=?
binding parameter [1] as [BIGINT] - [2]
delete from student where id=?
binding parameter [1] as [BIGINT] - [1]
...

DETACH

CascadeType.DETACH对应持久化上下文的detach和evict操作,这些操作可以将持久实体从持久上下文中移除,变成分离实体。

evict是detach操作的别名,两者没有什么区别。

示例:

public class Student {
	// ...
    @OneToMany(mappedBy = "student",
               cascade = {CascadeType.MERGE,
                          CascadeType.PERSIST,
                          CascadeType.REMOVE,
                          CascadeType.DETACH},
               fetch = FetchType.EAGER)
    private List<Email> emails = new ArrayList<>();
}

调用示例:

var savedIcexmoon = session.find(Student.class, icexmoon.getId());
Assertions.assertTrue(session.contains(savedIcexmoon));
savedIcexmoon.getEmails().forEach(e->{
    Assertions.assertTrue(session.contains(e));
});
session.detach(savedIcexmoon);
Assertions.assertFalse(session.contains(savedIcexmoon));
savedIcexmoon.getEmails().forEach(e->{
    Assertions.assertFalse(session.contains(e));
});

REFRESH

CascadeType.REFRESH对应持久化上下文从数据库中重新加载数据的操作,比如Session.refresh(...)。

示例:

public class Student {
    // ...
    @OneToMany(mappedBy = "student",
               cascade = {CascadeType.MERGE,
                          CascadeType.PERSIST,
                          CascadeType.REMOVE,
                          CascadeType.DETACH},
               fetch = FetchType.EAGER)
    @Builder.Default
    private List<Email> emails = new ArrayList<>();
}

调用示例:

var savedIcexmoon = session.find(Student.class, icexmoon.getId());
savedIcexmoon.setName("lalala");
var savedEmail = savedIcexmoon.getEmails().get(0);
savedEmail.setName("666").setDomain("gmail.com");
var oldEmailName = savedEmail.getName();
var oldEmailDomain = savedEmail.getDomain();
Assertions.assertEquals("lalala", savedIcexmoon.getName());
Assertions.assertEquals("666", savedEmail.getName());
Assertions.assertEquals("gmail.com", savedEmail.getDomain());
session.refresh(savedIcexmoon);
Assertions.assertEquals("icexmoon", savedIcexmoon.getName());
Assertions.assertEquals(oldEmailName, savedEmail.getName());
Assertions.assertEquals(oldEmailDomain, savedEmail.getDomain());

可以看到,调用Session.refresh后,关联到Student上的Email实例也被重新加载。

此外,Hibernate 提供了一些独特于 JPA 的级联类型,这些类型由枚举类型org.hibernate.annotations.CascadeType表示,大部分不同的级联类型已经作废,剩余的与 JPA 不同的级联类型有CascadeType.LOCK,要使用这些类型可以参考这篇文章。

The End,谢谢阅读。

本文的完整示例代码可以从这里获取。

参考资料

  • Overview of JPA/Hibernate Cascade Types. | Baeldung

  • 从零开始 Spring Boot 49:Hibernate Entity Lifecycle - 红茶的个人站点 (icexmoon.cn)

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

魔芋红茶

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

点赞
< 上一篇
下一篇 >

文章评论

取消回复

*

code

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

Theme Kratos Made By Seaton Jiang

宁ICP备2021001508号

宁公网安备64040202000141号