Що робить Propagation.NESTED
Якщо активна транзакція вже існує:
🟢 Junior Level
Propagation.NESTED — це тип поширення транзакцій у Spring Framework, який створює вкладену логічну транзакцію через механізм точок збереження (Savepoints) усередині вже наявної фізичної транзакції.
Суть у 30 секундах
Якщо активна транзакція вже існує:
- Spring не відкриває друге з’єднання з базою даних (на відміну від
REQUIRES_NEW), а створює JDBC Savepoint у поточному з’єднанні. - Якщо у вкладеному методі стається помилка, транзакція відкочується тільки до цієї точки збереження (
ROLLBACK TO SAVEPOINT). - Зовнішня транзакція може перехопити виняток у блоці
try-catchі продовжити роботу, успішно зафіксувавши решту операцій! - Проте якщо в результаті впаде зовнішня (батьківська) транзакція, то відкотяться абсолютно всі зміни, включаючи ті, які виконав
NESTED-метод.
Якщо активної транзакції не було — NESTED поводиться точно так само, як REQUIRED (відкриває нову фізичну транзакцію).
Зовнішня транзакція (REQUIRED)
|-- START TRANSACTION (Connection #1)
| INSERT INTO orders ... (Успіх)
|
| Вкладений виклик (NESTED):
| |-- SAVEPOINT nested_sp_1
| | INSERT INTO order_items ... (ПОМИЛКА!)
| |-- ROLLBACK TO SAVEPOINT nested_sp_1 <-- Відкат лише позицій!
|
| Зовнішній метод ловить помилку в catch:
| INSERT INTO audit_log ... (Успіх)
|-- COMMIT (Зберігаються orders та audit_log, але не order_items!)
Наочний приклад: обробка пакета замовлень із частковим збереженням
package com.example.service;
import com.example.dto.OrderDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Slf4j
@Service
@RequiredArgsConstructor
public class BatchOrderProcessor {
private final SingleOrderService singleOrderService;
@Transactional // Зовнішня транзакція: єдиний фізичний конект
public void processBatch(List<OrderDto> orders) {
for (OrderDto order : orders) {
try {
// Виклик вкладеного методу з точкою збереження (Savepoint)
singleOrderService.processSingleOrder(order);
} catch (Exception ex) {
// Відкотилося лише конкретне замовлення! Уся пачка не ламається!
log.error("Замовлення #{} не оброблено: {}", order.getId(), ex.getMessage());
}
}
// Фінальний COMMIT: фіксує всі успішно оброблені замовлення
}
}
package com.example.service;
import com.example.dto.OrderDto;
import com.example.repository.OrderJdbcRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
@RequiredArgsConstructor
public class SingleOrderService {
private final OrderJdbcRepository orderJdbcRepository;
@Transactional(propagation = Propagation.NESTED)
public void processSingleOrder(OrderDto order) {
orderJdbcRepository.save(order);
if (order.getAmount() <= 0) {
// Викидання винятку відкотить транзакцію лише до SAVEPOINT цього замовлення
throw new IllegalArgumentException("Сума замовлення повинна бути додатною");
}
}
}
🟡 Middle Level
Архітектура: як Spring реалізує NESTED через JDBC Savepoints
В основі роботи Propagation.NESTED лежить інтерфейс SavepointManager (реалізований у JdbcTransactionObjectSupport):
Виклик: batchService.process() -> singleService.processSingleOrder()
|
v
[ Spring AOP Proxy ]
|
v
[ TransactionInterceptor ]
|
1. Перевіряє TransactionSynchronizationManager: транзакція активна?
2. Викликає transactionStatus.createAndHoldSavepoint():
-> Connection connection = getDataSource().getConnection();
-> Savepoint sp = connection.setSavepoint("NESTED_SAVEPOINT_1");
|
v
[ Цільовий метод сервісу ]
|
-----------------------------------------
| При УСПІХУ: | При ВИНЯТКУ:
| connection.releaseSavepoint(sp); | connection.rollback(sp);
| (Savepoint звільняється, | (Відкат лише змін до точки,
| але COMMIT поки НЕ викликається!) | прапорець rollback-only НЕ виставляється!)
-----------------------------------------
|
v
Зовнішній метод завершується -> connection.commit()
(Фізичний коміт УСІХ успішних операцій у БД)
Порівняння трьох ключових типів: REQUIRED vs REQUIRES_NEW vs NESTED
| Критерій | REQUIRED |
REQUIRES_NEW |
NESTED |
|---|---|---|---|
| Кількість конектів до БД | 1 з’єднання | 2 незалежних з’єднання | 1 з’єднання |
| Фізичний коміт вкладеного методу | Разом із батьком | Негайно при виході з методу | Разом із батьком |
| Реакція на помилку у внутрішньому методі | Уся транзакція мітиться rollback-only |
Відкочується лише внутрішній метод | Відкочується до Savepoint |
| Якщо зовнішній метод упав ПІСЛЯ внутрішнього | Відкочується все | Внутрішній коміт ЗБЕРІГАЄТЬСЯ | Відкочується ВСЕ |
| Підтримка в Hibernate JPA | З коробки (100%) | З коробки (100%) | ⚠️ Обмежена (вимагає спец. налаштувань) |
| Підтримка в JTA (XA distributed tx) | Так | Так | ❌ Ні (JTA не підтримує Savepoint) |
| Небезпека Pool Deadlock | Ні | ⚠️ Висока при малому пулі HikariCP | Ні (використовується той самий конект) |
🔴 Senior Level
Проблема JPA / Hibernate L1 Cache при використанні Savepoint
Головна архітектурна причина, чому Propagation.NESTED рідко використовується в проєктах зі Spring Data JPA / Hibernate:
JDBC Savepoint відкочує стан сторінок і рядків у реляційній базі даних, але
EntityManager(кеш 1-го рівня /PersistenceContext) нічого не знає про цей відкат!
Покрокова катастрофа несинхронізованого кешу Hibernate:
1. singleService.processSingleOrder() [NESTED]
2. entityManager.persist(orderItem);
-> Hibernate реєструє сутність orderItem у PersistenceContext зі статусом MANAGED.
3. Внутрішній метод виконує entityManager.flush() -> в БД летить SQL INSERT.
4. Стається валідаційна помилка -> Spring виконує connection.rollback(savepoint).
-> У базі даних рядок INSERT фізично видалено!
5. Зовнішній метод ловить помилку в catch і продовжує роботу:
-> Але в пам'яті JVM в Hibernate PersistenceContext сутність orderItem ВСЕ ЩЕ ЧИСЛИТЬСЯ MANAGED!
6. Зовнішній метод завершується, спрацьовує фінальний flush() / commit():
-> Hibernate бачить сутність зі статусом MANAGED і намагається відправити SQL UPDATE або повторний INSERT,
що призводить до EntityNotFoundException, OptimisticLockException або порушення цілісності ключів!
Як безпечно готувати NESTED у Spring Data JPA
Якщо в проєкті з JPA дійсно необхідний частковий відкат, Senior Engineer вживає двох кроків:
Крок 1: Явне включення Savepoint у JpaTransactionManager
За замовчуванням JpaTransactionManager кидає NestedTransactionNotSupportedException. У конфігурації застосунку необхідно дозволити вкладені транзакції:
package com.example.config;
import jakarta.persistence.EntityManagerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
@Configuration
public class TransactionConfig {
@Bean
public PlatformTransactionManager transactionManager(EntityManagerFactory emf) {
JpaTransactionManager txManager = new JpaTransactionManager(emf);
// Примусово дозволяємо використання Savepoint у JPA:
txManager.setNestedTransactionAllowed(true);
return txManager;
}
}
Крок 2: Ручна зачистка L1 Cache (EntityManager.clear()) при помилці
Щоб видалити «фантомні» сутності з пам’яті після відкату Savepoint, викликаючий сервіс зобов’язаний очистити контекст персистентності:
package com.example.service;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class SafeNestedProcessor {
@PersistenceContext
private final EntityManager entityManager;
private final NestedItemService nestedItemService;
@Transactional
public void processSafeBatch(List<ItemDto> items) {
for (ItemDto item : items) {
try {
nestedItemService.processItem(item); // NESTED
} catch (Exception ex) {
// КРИТИЧНО ВАЖЛИВО: очистити L1 Cache, щоб відкочені
// сутності не спробували синхронізуватися при загальному коміті!
entityManager.clear();
}
}
}
}
4 Tricky Questions
1. Що станеться, якщо батьківська транзакція відкотиться ПІСЛЯ того, як вкладений метод з Propagation.NESTED успішно завершив свою роботу?
Відповідь:
Усі зміни, виконані успішним вкладеним методом, будуть повністю відкочені разом із батьківською транзакцією.
На відміну від Propagation.REQUIRES_NEW, де внутрішній метод комітиться фізично в окремому з’єднанні і його фіксація є незворотною, Propagation.NESTED утримує зміни в межах того самого відкритого фізичного з’єднання. При відкаті зовнішньої транзакції викликається глобальний connection.rollback(), який стирає всі зміни в транзакції незалежно від того, наскільки успішно були звільнені проміжні точки збереження (Savepoints).
2. Чому спроба викликати метод з Propagation.NESTED у стандартному застосунку зі Spring Boot + Hibernate призводить до NestedTransactionNotSupportedException?
Відповідь:
Тому що в JpaTransactionManager прапорець nestedTransactionAllowed за замовчуванням жорстко виставлений у false.
Розробники Spring Framework навмисно заблокували NESTED за замовчуванням для JPA, щоб захистити розробників від прихованої розсинхронізації пам’яті EntityManager (кеш першого рівня, dirty checking, карти snapshot) зі станом рядків у СУБД після відкату до точки збереження. Для безпечного застосування NESTED рекомендується використовувати чистий JDBC (DataSourceTransactionManager), JdbcTemplate, JOOQ або MyBatis.
3. Чи звільняються рядкові блокування (Row Locks) у базі даних при відкаті до точки збереження (ROLLBACK TO SAVEPOINT)?
Відповідь:
Так, блокування, захоплені всередині блока точки збереження, звільняються.
Коли СУБД (PostgreSQL, MySQL InnoDB, Oracle) виконує ROLLBACK TO SAVEPOINT sp:
- Усі зміни даних, виконані після створення точки
sp, відкочуються за журналом відкату (Undo Log / WAL). - Усі ексклюзивні блокування рядків (
X-locks), накладені запитамиUPDATE,DELETEабоSELECT FOR UPDATEпісля моменту створення точкиsp, негайно знімаються. - При цьому блокування рядків, захоплені батьківською транзакцією до створення
sp, залишаються на місці й утримуються до фінального коміту/відкату транзакції.
4. У чому фундаментальна різниця в обробці помилок між REQUIRED та NESTED, якщо викликаючий метод огортає виклик у try-catch?
Відповідь:
- При
REQUIRED: внутрішній метод виконується в тій самій транзакції. При виникненні винятку внутрішнійTransactionInterceptorперехоплює його і безумовно викликаєtransactionStatus.setRollbackOnly(). Коли батьківський метод перехоплює помилку вcatchі завершується штатно, Spring при коміті бачить прапорецьrollback-onlyі викидаєUnexpectedRollbackException, відкочуючи всю транзакцію цілком. - При
NESTED: внутрішнійTransactionInterceptorперехоплює виняток і викликаєconnection.rollback(savepoint). Він НЕ виставляє прапорецьrollback-onlyна глобальній транзакції. У результаті батьківський метод спокійно ловить виняток уcatch, а батьківська транзакція успішно комітиться.
🎯 Шпаргалка для інтерв’ю
30-секундна відповідь
«
Propagation.NESTEDстворює вкладену транзакцію з використанням точки збереження (JDBC Savepoint) у межах одного й того самого фізичного з’єднання. На відміну відREQUIRES_NEW, тут не витрачається додатковий конект із пулу і немає ризику Connection Pool Deadlock. При помилці у вкладеному методі відкат відбувається лише до точки збереження, дозволяючи зовнішній транзакції перехопити помилку й зафіксувати решту роботи. Проте якщо в кінці впаде зовнішня транзакція, відкотяться абсолютно всі дані, включаючи вкладені. У Hibernate JPANESTEDза замовчуванням вимкнений через розсинхронізацію L1-кешу».
Чек-лист ключових понять
- Механізм: JDBC Savepoint (
connection.setSavepoint(),connection.rollback(savepoint)). - Конекти до БД: Рівно 1 з’єднання (немає оверхеду пулу).
- Відкат батька: Відкочує всі вкладені Savepoint.
- Відкат вкладеного: Не ставить прапорець
rollback-onlyна батькові (дозволяє обробити вtry-catch). - JPA конфлікт:
JpaTransactionManagerкидаєNestedTransactionNotSupportedException, оскільки L1 Cache не вміє відкочуватися разом із БД.
Червоні прапорці (чого ні в якому разі не можна говорити)
- ❌ «NESTED відкриває другу фізичну транзакцію і бере другий конект» (Це робить
REQUIRES_NEW.NESTEDвикористовує Savepoint у поточному конекті). - ❌ «Якщо батьківська транзакція відкотиться, вкладений метод NESTED залишиться в базі» (При глобальному rollback відкотиться все).
- ❌ «NESTED працює в Spring Data JPA з коробки без жодних налаштувань» (Впаде з
NestedTransactionNotSupportedException).