Spring Boot 61:JPA 中的级联类型
图源:简书 (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)