1. 概述
在 Java 持久化开发中,实体之间的关系管理是常见需求。JPA 和 Hibernate 提供了级联(Cascade)机制来简化对关联实体的操作。本文将系统介绍 JPA 和 Hibernate 中的各类级联类型,并通过代码示例说明其行为差异。
适合阅读本文的读者应具备一定 JPA/Hibernate 基础,因此我们将略过基础概念,重点讲解级联类型的使用场景和注意事项。
2. 什么是级联(Cascading)?
当一个实体依赖于另一个实体存在时,比如 Person
和 Address
的关系,删除 Person
时我们也希望自动删除其关联的 Address
。这就是级联操作的意义所在。
✅ 级联的本质:对主实体执行操作时,自动将该操作传播到其关联的子实体。
3. JPA 级联类型
JPA 标准定义了以下级联类型,位于 javax.persistence.CascadeType
枚举中:
ALL
:所有操作都级联PERSIST
:保存时级联MERGE
:合并时级联REMOVE
:删除时级联REFRESH
:刷新时级联DETACH
:脱离时级联
4. Hibernate 级联类型
Hibernate 在 JPA 的基础上扩展了三个级联类型,定义在 org.hibernate.annotations.CascadeType
中:
REPLICATE
:复制操作级联SAVE_UPDATE
:保存或更新时级联LOCK
:锁定时级联
⚠️ 注意:Hibernate 的 CascadeType.DELETE
与 JPA 的 CascadeType.REMOVE
行为一致,两者可互换使用。
5. 各级联类型详解
以下通过代码和 SQL 输出说明每种级联类型的行为。
5.1. CascadeType.ALL
将所有操作传播到子实体。
@Entity
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String name;
@OneToMany(mappedBy = "person", cascade = CascadeType.ALL)
private List<Address> addresses;
}
子实体 Address
定义如下:
@Entity
public class Address {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private int id;
private String street;
private int houseNumber;
private String city;
private int zipCode;
@ManyToOne(fetch = FetchType.LAZY)
private Person person;
}
使用 CascadeType.ALL
后,对 Person
执行任意操作(如保存、更新、删除等),都会自动传播到其 Address
。
5.2. CascadeType.PERSIST
用于保存操作时级联。
@Test
public void whenParentSavedThenChildSaved() {
Person person = new Person();
Address address = new Address();
address.setPerson(person);
person.setAddresses(Arrays.asList(address));
session.persist(person);
session.flush();
session.clear();
}
输出 SQL:
Hibernate: insert into Person (name, id) values (?, ?)
Hibernate: insert into Address (city, houseNumber, person_id, street, zipCode, id) values (?, ?, ?, ?, ?, ?)
✅ 结论:PERSIST
级联会在保存 Person
时自动保存其 Address
。
5.3. CascadeType.MERGE
用于合并操作时级联。
@Test
public void whenParentSavedThenMerged() {
int addressId;
Person person = buildPerson("devender");
Address address = buildAddress(person);
person.setAddresses(Arrays.asList(address));
session.persist(person);
session.flush();
addressId = address.getId();
session.clear();
Address savedAddressEntity = session.find(Address.class, addressId);
Person savedPersonEntity = savedAddressEntity.getPerson();
savedPersonEntity.setName("devender kumar");
savedAddressEntity.setHouseNumber(24);
session.merge(savedPersonEntity);
session.flush();
}
输出 SQL:
Hibernate: select ... from Address where id=?
Hibernate: select ... from Person where id=?
Hibernate: update Address set ... where id=?
Hibernate: update Person set ... where id=?
✅ 结论:MERGE
级联会将父实体的合并操作传播到子实体。
5.4. CascadeType.REMOVE
用于删除操作时级联。
@Test
public void whenParentRemovedThenChildRemoved() {
int personId;
Person person = buildPerson("devender");
Address address = buildAddress(person);
person.setAddresses(Arrays.asList(address));
session.persist(person);
session.flush();
personId = person.getId();
session.clear();
Person savedPersonEntity = session.find(Person.class, personId);
session.remove(savedPersonEntity);
session.flush();
}
输出 SQL:
Hibernate: delete from Address where id=?
Hibernate: delete from Person where id=?
✅ 结论:REMOVE
级联会级联删除关联的子实体。
5.5. CascadeType.DETACH
用于脱离持久化上下文时级联。
@Test
public void whenParentDetachedThenChildDetached() {
Person person = buildPerson("devender");
Address address = buildAddress(person);
person.setAddresses(Arrays.asList(address));
session.persist(person);
session.flush();
assertThat(session.contains(person)).isTrue();
assertThat(session.contains(address)).isTrue();
session.detach(person);
assertThat(session.contains(person)).isFalse();
assertThat(session.contains(address)).isFalse();
}
✅ 结论:DETACH
级联会将子实体也从 Session 中移除。
5.6. CascadeType.LOCK
重新将实体及其子实体附加到持久化上下文。
@Test
public void whenDetachedAndLockedThenBothReattached() {
Person person = buildPerson("devender");
Address address = buildAddress(person);
person.setAddresses(Arrays.asList(address));
session.persist(person);
session.flush();
assertThat(session.contains(person)).isTrue();
assertThat(session.contains(address)).isTrue();
session.detach(person);
assertThat(session.contains(person)).isFalse();
assertThat(session.contains(address)).isFalse();
session.unwrap(Session.class)
.buildLockRequest(new LockOptions(LockMode.NONE))
.lock(person);
assertThat(session.contains(person)).isTrue();
assertThat(session.contains(address)).isTrue();
}
✅ 结论:LOCK
级联会将子实体重新附加到 Session。
5.7. CascadeType.REFRESH
用于刷新实体时级联。
@Test
public void whenParentRefreshedThenChildRefreshed() {
Person person = buildPerson("devender");
Address address = buildAddress(person);
person.setAddresses(Arrays.asList(address));
session.persist(person);
session.flush();
person.setName("Devender Kumar");
address.setHouseNumber(24);
session.refresh(person);
assertThat(person.getName()).isEqualTo("devender");
assertThat(address.getHouseNumber()).isEqualTo(23);
}
✅ 结论:REFRESH
级联会从数据库中重新加载子实体状态。
5.8. CascadeType.REPLICATE
用于多数据源同步场景。
@Test
public void whenParentReplicatedThenChildReplicated() {
Person person = buildPerson("devender");
person.setId(2);
Address address = buildAddress(person);
address.setId(2);
person.setAddresses(Arrays.asList(address));
session.unwrap(Session.class).replicate(person, ReplicationMode.OVERWRITE);
session.flush();
assertThat(person.getId()).isEqualTo(2);
assertThat(address.getId()).isEqualTo(2);
}
✅ 结论:REPLICATE
级联会将复制操作传播到子实体。
5.9. CascadeType.SAVE_UPDATE
用于 Hibernate 特有的 save
、update
、saveOrUpdate
操作。
@Test
public void whenParentSavedThenChildSaved() {
Person person = buildPerson("devender");
Address address = buildAddress(person);
person.setAddresses(Arrays.asList(address));
session.saveOrUpdate(person);
session.flush();
}
输出 SQL:
Hibernate: insert into Person (name, id) values (?, ?)
Hibernate: insert into Address (city, houseNumber, person_id, street, zipCode, id) values (?, ?, ?, ?, ?, ?)
✅ 结论:SAVE_UPDATE
级联适用于 Hibernate 的特有操作。
6. 总结
级联类型 | 说明 | 是否 JPA 标准 |
---|---|---|
ALL |
所有操作都级联 | ✅ |
PERSIST |
保存时级联 | ✅ |
MERGE |
合并时级联 | ✅ |
REMOVE |
删除时级联 | ✅ |
REFRESH |
刷新时级联 | ✅ |
DETACH |
脱离时级联 | ✅ |
REPLICATE |
复制时级联 | ❌(Hibernate) |
SAVE_UPDATE |
保存或更新时级联 | ❌(Hibernate) |
LOCK |
锁定时级联 | ❌(Hibernate) |
⚠️ 使用建议:
- 不要盲目使用
CascadeType.ALL
,除非你明确知道每个操作的含义。 - 小心
REMOVE
级联,它会级联删除子实体,可能导致数据丢失。 - 对于只读或非关键关系,避免使用级联。
✅ 源码地址:GitHub 示例项目