JavaRush /Курсы /Модуль 4. Работа с БД /Каскадные изменения

Каскадные изменения

Модуль 4. Работа с БД
13 уровень , 5 лекция
Открыта

6.1 Глубокое управление зависимостями

И еще немного полезного и интересного об аннотациях @OneToMany и им подобным. У них всех есть 4 часто используемых параметра:

  • cascade = CascadeType.ALL
  • orphanRemoval = true
  • fetch = FetchType.LAZY

Сейчас мы поподробнее их разберем. А начнем мы с самого интересного – CascadeType. Этот параметр определяет, что должно происходить с зависимыми сущностями, если мы меняем главную сущность.

В JPA спецификации есть такие значения этого параметра:

  • ALL
  • PERSIST
  • MERGE
  • REMOVE
  • REFRESH
  • DETACH

Однако Hibernate расширяет эту спецификацию еще на три варианта:

  • REPLICATE
  • SAVE_UPDATE
  • LOCK

Тут есть, конечно, сильная параллель с базой данных и их CONSTRANIS. Однако, есть и отличия. Hibernate старается по максимуму скрывать реальную работу с базой данных, так что эти Hibernate Cascades – это именно об Entity-объектах.

6.2 CascadeType

Параметр cascade описывает, что должно происходить с зависимыми объектами, если мы меняем их родительский (главный объект). Чаще всего этот параметр используется вместе с аннотациями, которые описывают зависимости объектов:

Пример:

OneToOne(cascade = CascadeType.ALL)

Или так:

@ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE})

Также его можно записать в виде отдельной аннотации:

@Cascade({ org.hibernate.annotations.CascadeType.ALL })

Теперь давай подробнее поговорим о том, что же эти аннотации значат.

6.3 ALL, PERSIST, MERGE

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

CascadeType.PERSIST означает, что если мы сохраняем в базу родительский объект, то это же нужно сделать и с его зависимыми объектами. Пример:


@Entity @Table(name="employee")
class Employee {
   @Column(name="id")
   public Integer id;
 
   @OneToOne(cascade = CascadeType.PERSIST, mappedBy="task")
   private EmployeeTask task;
}

Пример работы с этим классом:


Employee director = new Employee();
EmployeeTask task = new EmployeeTask();
director.task = task;
 
session.persist(director);
session.flush();

Мы сохраняем только объект типа Employee, его зависимый объект EmployeeTask будет сохранен в базу автоматически.

CascadeType.MERGE означает, что если мы обновляем в базе родительский объект, то это же нужно сделать и с его зависимыми объектами.

6.4 REMOVE, DELETE, DETACH

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

CascadeType.DELETE означает то же самое. Это синонимы. Просто из разных спецификаций.

CascadeType.DETACH означает, что если мы удаляем родительский объект из сессии, то это же нужно сделать и с его зависимыми объектами. Пример:


@Entity @Table(name="employee")
class Employee {
   @Column(name="id")
   public Integer id;
 
   @OneToOne(cascade = CascadeType.DETACH, mappedBy="task")
   private EmployeeTask task;
}

Пример работы с этим классом:


Employee director = new Employee();
EmployeeTask task = new EmployeeTask();
director.task = task;
director.task = task;
session.flush();
 
assertThat(session.contains(director)).isTrue();
assertThat(session.contains(task)).isTrue();
 
session.detach(director);
 
assertThat(session.contains(director)).isFalse();
assertThat(session.contains(task)).isFalse();

CascadeType.REFRESH и CascadeType.SAVE_UPDATE работают так же, как мы и ожидаем – дублируют действия, которые выполняются с родительским объектом к его зависимому объекту.

6.5 Параметр Orphan removal

Также иногда ты мог встретить параметр orphan. Это сокращение от термина Orphan removal. Он используется для того, чтобы не оставалось дочерних сущностей без родительских.

OneToOne(orphan = true)

Если этот параметр выставлен в true, то дочерняя сущность будет удалена, если на нее исчезли все ссылки. Это не совсем то же самое, что и Cascade.REMOVE.

У тебя может быть ситуация, когда несколько родительских сущностей ссылаются на одну дочернюю. Тогда выгодно, чтобы она удалялась не вместе с удалением родительской сущности, а только если все ссылки на нее будут обнулены.

Допустим, у тебя есть класс:


@Entity @Table(name="user")
class Employee {
   @Column(name="id")
   public Integer id;
 
   @OneToMany(cascade = CascadeType.ALL, orphan = true) @JoinColumn(name = "employee_id")
   private Set<EmployeeTask> tasks = new HashSet<EmployeeTask>();
}


Employee director = session.find(Employee.class, 4);
EmployeeTask task = director.tasks.get(0);
director.tasks.remove(task)
session.persist(director);
session.flush();

Объект EmployeeTask будет удален, так как на него не осталось ссылок. Родительский объект при этом никто не удалял.

6.6 Параметр fetch

Параметр fetch позволяет управлять режимами загрузки зависимых объектов. Обычно он принимает одно из двух значений:

  • FetchType.LAZY
  • FetchType.EAGER

Это очень интересная тема с различными подводными камнями, так что лучше я расскажу об этом в отдельной лекции.

Комментарии (7)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
Руслан Никитин Уровень 109
5 февраля 2025
CascadeType.SAVE_UPDATE - Этот тип является комбинацией PERSIST и MERGE. Если объект еще не сохранен, он будет сохранен, а если уже существует, то будет обновлен
ivan Уровень 40
24 декабря 2023
очень хорошая статья. рад что javarush приносит пользу даже после прохождения курса и окончания подписки
Dmytryi Shubchynskyi Уровень 111 Expert
31 марта 2023
Тут видимо ошибка

Employee director = new Employee();
EmployeeTask task = new EmployeeTask();
director.task = task;
director.task = task;
session.flush();
RISTA SURVIVAL Уровень 8
17 марта 2023
Пункт 6.5 Почему в Employee стоит аннотация @JoinColumn ведь она стоит обычно, где стоит аннотация @ManyToOne. Также стоит там имя колонки employee_id ведь это означает что будет создана колонка employee_id в классе Employee, а это странно. Или это второй вариант как заменить модель, где используется JoinColumn в классе, где внешний ключ и есть в классе родительском MappedBy. И то есть это типо заменяет этот вариант при котором в классе EmployeeTask создается колонка employee_id но прописываем это в Employee и также ставим аннотацию JoinColumn и убираем MappedBy в этом же классе, верно я понял, если нет обьясните почему так здесь написано, а раньше подругому писали

@Entity
@Table(name="user")
class Employee {
   @Column(name="id")
   public Integer id;

   @OneToMany(cascade = CascadeType.ALL, orphan = true)
   @JoinColumn(name = "employee_id")
   private Set<EmployeeTask> tasks = new HashSet<EmployeeTask>();
}
Anonymous #3613348 Уровень 90
3 июня 2026
Во-первых, здесь должно быть @Table(name="employee"), а не "user". Во-вторых, в данной конфигурации @JoinColumn(name = "employee_id") указывает на имя внешнего ключа (Foreign Key) в таблице дочерней сущности (EmployeeTask), а не родительской таблицы employee. При однонаправленной связи @OneToMany аннотация @JoinColumn меняет свое стандартное поведение: Она не создает колонку employee_id в таблице employee. Вместо этого она идет в связанную таблицу (дочернюю) для EmployeeTask и создает колонку employee_id там. В базе данных у вас получатся следующие таблицы:

-- Таблица сотрудников (родительская)
CREATE TABLE employee (
    id INT PRIMARY KEY
);

-- Таблица задач (дочерняя)
CREATE TABLE employee_task (
    id INT PRIMARY KEY,
    employee_id INT, -- Вот эта колонка! Она создана благодаря @JoinColumn(name = "employee_id")
    FOREIGN KEY (employee_id) REFERENCES employee(id)
);
RISTA SURVIVAL Уровень 8
17 марта 2023
Пункт 6.3 Почему в MappedBy в качестве аргумента стоит task, если во всех остальных примерах был employee? Да и просто это не логично, ведь исходя из всех ваших описаний класса EmployeeTask, там вообще нет такого поля task. Ведь MappedBy ссылается на поле класса собственника, где есть аннотация @ManyToOne или я в чем-то не прав? Просто по моему мнению там должно быть employee, а не task Или это какой-то другой вариант описания таблиц, в котором нужно просто обозначить типо поле обратного класса. Даже если такого поля в классе том нет, но в MappedBy. Просто объясните, это опечатка и тогда как верно или это какой-то другой вариант написания. Так как примеров подобных пока не нашел. И также добавьте обратные классы с которыми типо идет взаимодействие. А то так не понятно выходит. Сам EmployeeTask добавьте, пожалуйста

@Entity
@Table(name="employee")
class Employee {
   @Column(name="id")
   public Integer id;

   @OneToOne(cascade = CascadeType.DETACH, mappedBy="task")
   private EmployeeTask task;
}
Anonymous #3613348 Уровень 90
3 июня 2026
"Просто по моему мнению там должно быть employee, а не task" Похоже, что так. По предыдущим лекциям внешний ключ в таблице employee_task, значит пример, наверное, будет таким:

@Entity 
class Employee {
   @Id 
   public Integer id;

   // mappedBy указывает на поле 'employee' из класса EmployeeTask!
   @OneToOne(cascade = CascadeType.PERSIST, mappedBy = "employee") 
   private EmployeeTask task; 
}

@Entity
class EmployeeTask {
   @Id 
   public Integer id;

   @OneToOne
   @JoinColumn(name = "employee_id") // Указываем колонку здесь
   private Employee employee; // Главное поле связи, на которое ссылается mappedBy
}
В 6.3 разбирается двусторонняя связь, а если мы хотим создать двустороннюю (bidirectional) связь, аннотация @OneToOne обязана стоять в обоих классах. Каждая сторона выполняет свою строго определенную роль: 1. Управляющая сторона (Owner of the relationship). Это класс, который физически отвечает за связь в базе данных. В его таблице будет создана колонка с внешним ключом (Foreign Key). Здесь ставится @OneToOne и обычно добавляется @JoinColumn. 2. Ведомая сторона (Inverse side). Это класс, который просто «знает» о существовании связи, но сам в своей таблице ничего не хранит. Здесь ставится @OneToOne(mappedBy = "..."). Параметр mappedBy как раз и говорит: «Я не главный, ищи настройки связи на противоположной стороне в указанном поле». Ну и пример java-кода, предложенный ИИ, как по мне, удачнее: Когда связь двусторонняя, в Java нужно синхронизировать обе стороны вручную при назначении задачи, иначе Hibernate может не сохранить связь в БД:

Employee director = new Employee();
EmployeeTask task = new EmployeeTask();

// Устанавливаем связь в обе стороны!
director.setTask(task);
task.setEmployee(director);