Які типи Cascade існують
Специфікація JPA (Jakarta Persistence) та фреймворк Hibernate надають набір типів каскадування, що визначають, які саме операції керування життєвим циклом сутності автоматично т...
Специфікація JPA (Jakarta Persistence) та фреймворк Hibernate надають набір типів каскадування, що визначають, які саме операції керування життєвим циклом сутності автоматично транслюються від батьківського об’єкта до пов’язаних дочірніх сутностей. Розуміння специфіки кожного типу каскаду дозволяє вибудовувати надійні межі агрегатів за канонами Domain-Driven Design (DDD) та запобігати випадковому видаленню спільних даних.
🟢 Junior Level
6 стандартних типів CascadeType в JPA
У переліку jakarta.persistence.CascadeType визначено шість базових значень:
| Тип каскаду | Якому виклику EntityManager відповідає |
Поведінка при виклику на батьківській сутності |
|---|---|---|
PERSIST |
entityManager.persist(parent) |
Нові дочірні об’єкти (Transient) також передаються в persist() і отримують статус Managed. |
MERGE |
entityManager.merge(parent) |
Від’єднані дочірні об’єкти (Detached) також об’єднуються з поточною сесією. |
REMOVE |
entityManager.remove(parent) |
Дочірні записи видаляються з бази даних при видаленні батьківського рядка. |
REFRESH |
entityManager.refresh(parent) |
Дані всіх дочірніх сутностей примусово перезавантажуються з БД. |
DETACH |
entityManager.detach(parent) |
Усі пов’язані об’єкти виключаються з кешу L1 і переходять у стан Detached. |
ALL |
Усі операції одразу | Активує всі 5 вищеперелічених стратегій каскадування одночасно. |
// Приклад комбінування типів каскаду:
@Entity
public class Document {
@Id
private Long id;
// Дозволяємо зберігати й оновлювати вкладення разом із документом,
// але забороняємо автоматичне видалення:
@OneToMany(mappedBy = "document", cascade = {CascadeType.PERSIST, CascadeType.MERGE})
private List<Attachment> attachments = new ArrayList<>();
}
🟡 Middle Level
Нативні типи каскадування Hibernate (org.hibernate.annotations.CascadeType)
Крім стандарту JPA, бібліотека Hibernate надає власний розширений набір анотацій каскадування @org.hibernate.annotations.Cascade:
SAVE_UPDATE(Legacy Hibernate): Каскадує викликиsession.saveOrUpdate().LOCK: Каскадно накладає блокуванняsession.lock()на пов’язані сутності.REPLICATE: Каскадно переносить сутності між різними базами даних черезsession.replicate().
Рекомендація: У сучасному коді слід завжди використовувати стандартний JPA jakarta.persistence.CascadeType, уникаючи прив’язки до вендорських анотацій Hibernate.
Архітектурне правило: Композиція проти Асоціації
Вибір типу каскадування повинен суворо слідувати моделі предметної області (DDD):
СТРАТЕГІЯ КАСКАДУВАННЯ ЗА ТИПОМ ЗВ'ЯЗКУ
1. КОМПОЗИЦІЯ (Parent володіє Child монопольно)
Order ──────[1:N]──────> OrderItem
Invoice ────[1:N]──────> InvoiceLine
• Стратегія: cascade = CascadeType.ALL, orphanRemoval = true
• Обґрунтування: OrderItem не існує у відриві від Order.
2. АСОЦІАЦІЯ (Зв'язок між незалежними сутностями)
Order ──────[N:1]──────> Customer
Product ────[N:M]──────> Category
• Стратегія: БЕЗ cascade (або тільки PERSIST/MERGE за необхідності)
• Обґрунтування: Видалення замовлення НЕ ПОВИННО видаляти клієнта!
🔴 Senior Level
Пастка розділення PERSIST та MERGE
Поширена помилка проєктування: розробник вказує cascade = CascadeType.PERSIST, забуваючи про CascadeType.MERGE:
@Entity
public class Order {
@Id
private Long id;
@OneToMany(mappedBy = "order", cascade = CascadeType.PERSIST)
private List<OrderItem> items = new ArrayList<>();
}
Що відбувається в коді сервісу:
// 1. Отримали detached-замовлення (наприклад, з REST API)
Order detachedOrder = getOrderFromApi();
detachedOrder.getItems().get(0).setPrice(BigDecimal.TEN);
// 2. У транзакції викликаємо збереження:
Order managedOrder = entityManager.merge(detachedOrder);
- Оскільки у колекції
itemsвказано тількиCascadeType.PERSIST, методmerge()проігнорує дочірню колекцію! - Батько
Orderоновиться, але змінена ціна вOrderItemне буде збережена в базі даних! - Правило: Якщо сутність може оновлюватися в detached-стані, каскади
PERSISTтаMERGEпрактично завжди повинні йти в парі:cascade = {CascadeType.PERSIST, CascadeType.MERGE}(абоCascadeType.ALL).
Небезпеки каскаду REMOVE у відносинах @ManyToMany
@Entity
public class Student {
@Id
private Long id;
// ❌ КАТАСТРОФА: Відрахування студента видалить усі курси з університету!
@ManyToMany(cascade = CascadeType.ALL)
private List<Course> courses = new ArrayList<>();
}
Якщо відрахувати одного студента через entityManager.remove(student), Hibernate видалить не просто зв’язувальний рядок у таблиці student_courses, а фізично видалить рядки з таблиці courses! А якщо в курсу Course також стояв каскад ALL до студентів, Hibernate каскадно зітре взагалі всіх інших студентів цього курсу.
Стандарт: Для @ManyToMany каскад REMOVE категорично заборонений. Дозволені лише PERSIST та MERGE.
4 Tricky Questions
1. Якщо на зв’язку стоїть лише CascadeType.PERSIST, чи спрацює збереження дочірнього елемента при виклику repository.save(parent) у Spring Data JPA, якщо батько вже існує в БД?
Відповідь:
- Ні, не спрацює!
- У Spring Data JPA метод
save(entity)перевіряє, чи є сутність новою (isNew()). - Оскільки батько вже існує в базі (
id != null), Spring Data викликає методentityManager.merge(parent), а неpersist(). - Оскільки на зв’язку оголошено лише
CascadeType.PERSIST, методmerge()не буде каскадуватися на дочірні елементи. Нові дочірні об’єкти залишаться в станіTransient, і СУБД викине помилкуTransientPropertyValueException.
2. Що станеться, якщо батьківська сутність містить каскад CascadeType.ALL на дочірню колекцію, але в базі даних на зовнішньому ключі налаштовано правило ON DELETE RESTRICT?
Відповідь:
- При виклику
entityManager.remove(parent)Hibernate виконає каскадний обхід і сформує вActionQueueсписок командDELETEдля кожного дочірнього елемента, а потім — для батька. - Оскільки Hibernate виконує видалення дочірніх записів раніше, ніж батьківського рядка, обмеження бази даних
ON DELETE RESTRICTне буде порушено, і видалення завершиться успішно. - Помилка СУБД виникла б лише в тому разі, якби видалення виконувалося прямим нативним SQL-запитом до батьківського рядка без попереднього очищення дітей.
3. Припустимо, ми викликаємо entityManager.refresh(parent), а на зв’язку вказано CascadeType.REFRESH. Що станеться з незбереженими змінами в дочірніх об’єктах?
Відповідь:
- Каскад
REFRESHпошириться на кожну дочірню сутність, яка знаходиться в пам’яті. - Для кожного дочірнього об’єкта Hibernate виконає SQL
SELECT * FROM child_table WHERE id = ?. - Усі незбережені локальні зміни, зроблені в полях дочірніх об’єктів в оперативній пам’яті JVM, будуть безповоротно стерті актуальними значеннями колонок із бази даних.
4. Чому каскад CascadeType.DETACH не захищає від LazyInitializationException після закриття сесії?
Відповідь:
CascadeType.DETACHпереводить об’єкти зі стануManagedуDetached.- Якщо пов’язана лінива колекція не була завантажена в пам’ять до виклику
detach(), вона так і залишається неініціалізованим проксі-об’єктомPersistentBag. - Оскільки сесія від’єднується, проксі втрачає доступ до активного JDBC-з’єднання.
- Подальше звернення до гетера колекції на від’єднаному об’єкті гарантовано викине
LazyInitializationException. КаскадDETACHкерує статусом сутностей у контексті, але не форсує завантаження даних із бази.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь (Elevator Pitch)
«У JPA існує 6 типів каскадів (
CascadeType):PERSIST,MERGE,REMOVE,REFRESH,DETACHтаALL. Головний архітектурний принцип вибору каскадів базується на концепції агрегатів DDD: для композицій@OneToMany(де дочірні об’єкти не мають сенсу без батька, якOrder->OrderItem) використовується зв’язкаCascadeType.ALLразом ізorphanRemoval = true. Для асоціацій@ManyToOneта@ManyToMany(де сутності незалежні) каскадREMOVEсуворо заборонений задля уникнення видалення спільних даних. Важливо пам’ятати, щоPERSISTіMERGEдіють незалежно: якщо в Spring Data JPA викликатиsave()на існуючому батьку з каскадом лишеPERSIST, дочірні об’єкти не збережуться».
Ключові метрики для Production-моніторингу
- Cascade Tree Traversal Depth: глибина каскадного обходу графа об’єктів (занадто глибокі дерева сповільнюють коміт транзакції).
- Кількість SQL-операцій на один виклик
save()/remove(): дозволяє вчасно виявити надлишковий каскадний оверхед.
Червоні прапорці (Чого говорити НЕ МОЖНА)
- ❌ «Я завжди ставлю CascadeType.ALL на всі анотації @ManyToOne» — Це призведе до видалення спільних користувачів або довідників при видаленні дочірніх записів.
- ❌ «CascadeType.PERSIST автоматично включає в себе і CascadeType.MERGE» — Неправда. Це абсолютно незалежні операції життєвого циклу.
- ❌ «Каскад REMOVE на зв’язку @ManyToMany видалить лише зв’язувальну таблицю» — Ні, він фізично видалить цільові сутності з їхніх основних таблиць.
- ❌ «Використовую CascadeType.ALL для зв’язку Order -> Customer» — Порушення меж агрегатів, замовлення не володіє клієнтом.