Перейти к основному содержимому

Обзор каскадных типов JPA/Hibernate

· 6 мин. чтения

1. Обзор

В этом руководстве мы обсудим, что такое каскадирование в JPA/Hibernate. Затем мы рассмотрим различные доступные типы каскадов, а также их семантику.

2. Что такое каскадирование?

Отношения сущностей часто зависят от существования другой сущности, например, отношения ЛицоАдрес . Без Person сущность Address не имеет собственного значения. Когда мы удаляем сущность Person , наша сущность Address также должна быть удалена.

Каскадирование - это способ добиться этого. Когда мы выполняем какое-либо действие над целевым объектом, то же самое действие будет применено к связанному объекту.

2.1. Каскадный тип JPA

Все специфичные для JPA каскадные операции представлены перечислением javax.persistence.CascadeType , содержащим записи:

  • ВСЕ
  • СОПРОТИВЛЯТЬСЯ
  • ОБЪЕДИНИТЬ
  • УДАЛЯТЬ
  • ОБНОВИТЬ
  • ОТСОЕДИНИТЬ

2.2. Каскадный режим гибернации

Hibernate поддерживает три дополнительных каскадных типа наряду с указанными JPA. Эти специфичные для Hibernate каскадные типы доступны в org.hibernate.annotations.CascadeType :

  • РЕПЛИКАЦИЯ
  • SAVE_UPDATE
  • ЗАМОК

3. Разница между типами каскадов

3.1. Каскадный тип . ВСЕ

CascadeType.ALL распространяет все операции, в том числе специфичные для Hibernate, от родительского объекта к дочернему.

Давайте посмотрим на это на примере:

@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;
}

Обратите внимание, что в ассоциациях OneToMany мы упомянули каскадный тип в аннотации.

Теперь давайте посмотрим на связанный объект 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;
}

3.2. Каскадный тип . СОПРОТИВЛЯТЬСЯ

Операция persist делает временный экземпляр постоянным. Каскадный тип 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 (?, ?, ?, ?, ?, ?)

3.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 address0_.id as id1_0_0_, address0_.city as city2_0_0_, address0_.houseNumber as houseNum3_0_0_, address0_.person_id as person_i6_0_0_, address0_.street as street4_0_0_, address0_.zipCode as zipCode5_0_0_ from Address address0_ where address0_.id=?
Hibernate: select person0_.id as id1_1_0_, person0_.name as name2_1_0_ from Person person0_ where person0_.id=?
Hibernate: update Address set city=?, houseNumber=?, person_id=?, street=?, zipCode=? where id=?
Hibernate: update Person set name=? where id=?

Здесь мы видим, что операция слияния сначала загружает как адреса, так и лица , а затем обновляет их в результате CascadeType.MERGE .

3.4. CascadeType.REMOVE

Как следует из названия, операция удаления удаляет строку, соответствующую объекту, из базы данных, а также из постоянного контекста.

CascadeType.REMOVE распространяет операцию удаления от родительского к дочернему объекту. Подобно CascadeType.REMOVE JPA , у нас есть CascadeType.DELETE , специфичный для Hibernate. Между ними нет никакой разницы.

Теперь пришло время протестировать 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=?

Адрес , связанный с человеком, также был удален в результате CascadeType.REMOVE .

3.5. CascadeType.DETACH

Операция отсоединения удаляет объект из постоянного контекста. Когда мы используем 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();
}

Здесь мы видим, что после отсоединения person в постоянном контексте не существует ни человека , ни адреса .

3.6. Каскадный тип . ЗАМОК

Неинтуитивно CascadeType.LOCK снова присоединяет объект и связанный с ним дочерний объект к постоянному контексту.

Давайте посмотрим на тестовый пример, чтобы понять 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();
}

Как мы видим, при использовании CascadeType.LOCK мы прикрепляем объект person и связанный с ним адрес обратно к постоянному контексту.

3.7. Каскадный тип . ОБНОВИТЬ

Операции обновления повторно считывают значение данного экземпляра из базы данных. В некоторых случаях мы можем изменить экземпляр после сохранения в базе данных, но позже нам нужно будет отменить эти изменения.

В таком сценарии это может быть полезно. Когда мы используем эту операцию с каскадным типом REFRESH , дочерняя сущность также перезагружается из базы данных всякий раз, когда обновляется родительская сущность.

Для лучшего понимания давайте посмотрим тестовый пример для 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);
}

Здесь мы внесли некоторые изменения в сохраненные объекты person и address . Когда мы обновляем сущность человека , адрес также обновляется.

3.8. CascadeType.REPLICATE

Операция репликации используется, когда у нас есть более одного источника данных и мы хотим, чтобы данные были синхронизированы. При использовании CascadeType.REPLICATE операция синхронизации также распространяется на дочерние сущности всякий раз, когда выполняется над родительской сущностью.

Теперь давайте протестируем CascadeType. РЕПЛИКАЦИЯ :

@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);
}

Из-за CascadeType . REPLICATE , когда мы реплицируем сущность человека , связанный с ней адрес также реплицируется с установленным нами идентификатором.

3.9. CascadeType.SAVE_UPDATE

CascadeType.SAVE_UPDATE распространяет ту же операцию на связанный дочерний объект. Это полезно, когда мы используем специфичные для Hibernate операции, такие как save , update и saveOrUpdate .

Давайте посмотрим CascadeType. SAVE_UPDATE в действии:

@Test
public void whenParentSavedThenChildSaved() {
Person person = buildPerson("devender");
Address address = buildAddress(person);
person.setAddresses(Arrays.asList(address));
session.saveOrUpdate(person);
session.flush();
}

Из-за CascadeType.SAVE_UPDATE , когда мы запускаем приведенный выше тестовый пример, мы видим, что человек и адрес были сохранены.

Вот полученный SQL:

Hibernate: insert into Person (name, id) values (?, ?)
Hibernate: insert into Address (
city, houseNumber, person_id, street, zipCode, id) values (?, ?, ?, ?, ?, ?)

4. Вывод

В этой статье мы обсудили каскадирование и различные параметры каскадного типа, доступные в JPA и Hibernate.

Исходный код статьи доступен на GitHub .