Що робить Propagation.NESTED
Spring реалізує NESTED через JDBC Savepoints (не через нові транзакції):
🟢 Junior Level
Propagation.NESTED — це тип поширення транзакцій в Spring, який створює “вкладену” транзакцію всередині вже існуючої. Якщо зовнішня транзакція відкочується, вкладена теж відкотиться. Але якщо вкладена відкотиться — зовнішня може продовжити роботу.
Проста аналогія: Ви пишете документ (зовнішня транзакція). Періодично ви робите “зберегти версію” (savepoint). Якщо нова правка зіпсувала документ, ви можете відкотитися до останньої версії (savepoint), не втрачаючи всю попередню роботу. Але якщо ви вирішите викинути весь документ — всі версії теж зникнуть.
SQL-приклад (JDBC Savepoint):
@Service
public class BatchImportService {
@Autowired private NestedImportService nestedImportService;
@Transactional
public void importAll(List<Data> dataList) {
for (Data data : dataList) {
try {
// Кожен запис — вкладена "міні-транзакція"
nestedImportService.importOne(data);
} catch (Exception e) {
// Тільки цей запис відкотиться, інші продовжать
log.warn("Failed to import: {}", data.getId());
}
}
// В кінці — один загальний COMMIT для всіх успішних записів
}
}
@Service
public class NestedImportService {
@Transactional(propagation = Propagation.NESTED)
public void importOne(Data data) {
repo.save(data);
// Якщо виняток → rollback to savepoint (не весь batch)
}
}
Ключова відмінність від REQUIRES_NEW:
- NESTED: Один COMMIT в самому кінці, вкладена — це savepoint всередині тієї ж транзакції
- REQUIRES_NEW: Два незалежних COMMIT, внутрішня транзакція фіксується одразу
Коли використовувати:
- Обробка batch-даних з допустимими partial failures
- Коли потрібно “врятувати” частину роботи, але фінальний commit має бути атомарним
🟡 Middle Level
Як це працює всередині
Spring реалізує NESTED через JDBC Savepoints (не через нові транзакції):
Виклик: outerMethod() → innerMethod(NESTED)
1. Spring перевіряє: чи є активна транзакція? → Так
2. Spring створює JDBC Savepoint:
Connection conn = dataSource.getConnection();
Savepoint sp = conn.setSavepoint("NESTED_SAVEPOINT_1");
3. Виконується innerMethod()
4. Якщо успіх → savepoint released (але НЕ commit!)
5. Якщо виняток → conn.rollback(sp) → відкат до savepoint
6. В кінці outerMethod() → один загальний conn.commit()
Важливо: NESTED працює тільки з DataSourceTransactionManager та JpaTransactionManager. Він НЕ працює з JTA в режимі savepoint. JTA (Java Transaction API) — API для розподілених транзакцій, що охоплюють кілька БД або систем. Spring автоматично fallback-ить до REQUIRES_NEW поведінки. Тому для JTA краще вказувати REQUIRES_NEW явно.
Практичне застосування
Сценарій 1: Batch import з partial failure tolerance
@Service
public class CsvImportService {
@Autowired private RowImportService rowImportService;
@Transactional
public ImportResult importCsv(MultipartFile file) {
int success = 0;
int failed = 0;
List<String> rows = parseCsv(file);
for (String row : rows) {
try {
rowImportService.importRow(row); // NESTED
success++;
} catch (ValidationException e) {
failed++;
// Цей рядок відкотився до savepoint, продовжуємо
}
}
return new ImportResult(success, failed);
// COMMIT — всі успішні рядки зберігаються
}
}
@Service
public class RowImportService {
@Transactional(propagation = Propagation.NESTED)
public void importRow(String row) {
Data data = validateAndParse(row); // може кинути ValidationException
repo.save(data);
}
}
Сценарій 2: Оркестрація з compensation
@Service
public class OrderOrchestrator {
@Autowired private InventoryService inventoryService;
@Autowired private PaymentService paymentService;
@Autowired private NotificationService notificationService;
@Transactional
public OrderResult createOrder(Order order) {
try {
inventoryService.reserve(order.getItems()); // NESTED
} catch (Exception e) {
return OrderResult.failed("No stock");
}
try {
paymentService.charge(order.getTotal()); // NESTED
} catch (Exception e) {
// Payment failed — reservation теж відкотиться (загальна транзакція)
throw new OrderException("Payment failed", e);
}
// Все успішно — notification (REQUIRED, частина загальної транзакції)
notificationService.sendOrderConfirmation(order);
return OrderResult.success();
// COMMIT — всі зміни атомарні
}
}
Типові помилки
| Помилка | Наслідок | Рішення |
|---|---|---|
| NESTED без активної зовнішньої транзакції | Поводиться як REQUIRED (створює нову) | Переконатися, що зовнішній метод @Transactional |
| NESTED + Hibernate L1 cache inconsistency | Stale entities після savepoint rollback | em.flush() + em.clear() після rollback |
| NESTED з JTA | Не підтримується — fallback до REQUIRES_NEW | Використовувати REQUIRES_NEW явно для JTA |
| Припущення, що NESTED = REQUIRES_NEW | Внутрішні зміни НЕ видні іншим tx до загального commit | Розуміти різницю: NESTED = savepoint, REQUIRES_NEW = нова tx |
| Довга транзакція з множиною savepoints | Накопичення undo log, memory pressure | Лімітувати кількість nested викликів |
Порівняння: NESTED vs REQUIRES_NEW
| Характеристика | NESTED | REQUIRES_NEW |
|---|---|---|
| Фізичних транзакцій | 1 | 2 |
| Механізм | JDBC Savepoint | Suspend + нова транзакція |
| Commit внутрішньої | Ні (тільки release savepoint) | Так, негайний |
| Rollback внутрішньої | To savepoint | Повна внутрішня tx |
| Видимість даних іншим tx | Тільки після загального commit | Одразу після внутрішнього commit |
| Rollback зовнішньої tx | Відкочує внутрішню | Не впливає на внутрішню |
| Підтримка JTA | Ні | Так |
| Hibernate L1 cache | Проблеми при rollback | Без проблем |
| Connection usage | 1 connection | 2 connections (suspend + new) |
| Overhead | Низький (~1-3ms) | Середній (~5-15ms) |
Коли НЕ варто використовувати NESTED
- JTA / distributed transactions — не підтримується
- Коли внутрішня транзакція має коммітитися незалежно — використовуйте REQUIRES_NEW
- З Hibernate без flush/clear — risk of L1 cache inconsistency
- Для аудиту — аудит зазвичай потрібен незалежно від outer tx, краще REQUIRES_NEW
Коли NESTED vs REQUIRES_NEW
- Аудит/логування = REQUIRES_NEW (дані мають вижити при outer rollback)
- Batch processing з partial failure = NESTED (єдиний commit, один connection)
- Distributed transaction = REQUIRES_NEW (NESTED не підтримує JTA)
- Потрібно бачити inner data з інших tx = REQUIRES_NEW (NESTED видимий тільки після загального commit)
🔴 Senior Level
Internal Implementation: Savepoint Mechanics та Spring Integration
JdbcTransactionObjectSupport — Savepoint Management
// org.springframework.jdbc.datasource.JdbcTransactionObjectSupport
// Spring Framework 6.x
public class JdbcTransactionObjectSupport implements SavepointManager, SmartTransactionObject {
private ConnectionHolder connectionHolder;
private Savepoint currentSavepoint;
private Integer previousIsolationLevel;
private boolean rollbackOnly = false;
private boolean savepointAllowed = false;
@Override
public Object createSavepoint(String name) throws TransactionException {
try {
if (!this.savepointAllowed) {
throw new NestedTransactionNotSupportedException(
"Cannot create savepoint — no active transaction or not supported");
}
// JDBC 3.0 API
this.currentSavepoint = getConnection().setSavepoint(name);
return this.currentSavepoint;
} catch (SQLException ex) {
throw new CannotCreateSavepointException("Could not create JDBC savepoint", ex);
}
}
@Override
public void rollbackToSavepoint(Object savepoint) throws TransactionException {
try {
Savepoint sp = (Savepoint) savepoint;
// JDBC rollback to savepoint
getConnection().rollback(sp);
// Важливо: connection залишається активним, транзакція продовжується
this.currentSavepoint = null;
} catch (SQLException ex) {
throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex);
}
}
@Override
public void releaseSavepoint(Object savepoint) throws TransactionException {
try {
Savepoint sp = (Savepoint) savepoint;
getConnection().releaseSavepoint(sp);
// Non-fatal if DB auto-releases on commit (MySQL)
} catch (SQLException ex) {
// Log but don't fail — some databases release savepoints automatically
}
}
}
AbstractPlatformTransactionManager — NESTED Handling
// Ключовий фрагмент з handleExistingTransaction()
case PROPAGATION_NESTED:
if (!isNestedTransactionAllowed()) {
throw new NestedTransactionNotSupportedException(
"Transaction manager does not allow nested transactions");
}
if (useSavepointForNestedTransaction()) {
// DataSourceTransactionManager, JpaTransactionManager → savepoint
DefaultTransactionStatus status =
prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
// Створюємо savepoint
status.createAndHoldSavepoint();
return status;
} else {
// JTA → немає savepoint support → fallback до REQUIRES_NEW
SuspendedResourcesHolder suspendedResources = suspend(transaction);
// ... створює нову транзакцію
}
useSavepointForNestedTransaction() Decision
// DataSourceTransactionManager → true (підтримує savepoints)
// JpaTransactionManager → true (делегування до JDBC)
// JtaTransactionManager → false (JTA не підтримує savepoints)
// HibernateTransactionManager → true (через JDBC connection)
protected boolean useSavepointForNestedTransaction() {
return (getDataSource() != null); // Спрощено
}
Savepoint Implementation в різних БД
PostgreSQL:
BEGIN;
SAVEPOINT sp1;
INSERT INTO orders (id, amount) VALUES (1, 100);
-- Rollback to savepoint
ROLLBACK TO SAVEPOINT sp1;
-- orders порожній — insert скасовано, але транзакція активна
INSERT INTO orders (id, amount) VALUES (2, 200);
COMMIT;
-- orders: (2, 200)
-- PostgreSQL savepoints:
-- - Зберігаються в пам'яті (не на диску)
-- - Можуть бути nested (savepoint всередині savepoint)
-- - Auto-released на commit
MySQL/InnoDB:
START TRANSACTION;
SAVEPOINT sp1;
INSERT INTO orders VALUES (1, 100);
ROLLBACK TO SAVEPOINT sp1;
-- В MySQL: auto-release savepoint після rollback!
-- Повторний ROLLBACK TO SAVEPOINT sp1 → ERROR
INSERT INTO orders VALUES (2, 200);
COMMIT;
-- MySQL savepoints:
-- - Auto-released після ROLLBACK TO (не можна reuse)
-- - Не логуються в binlog (statement-based replication safe)
-- - Ця поведінка стабільна across all supported versions MySQL 5.7+ та PostgreSQL 9+.
-- Spring генерує унікальні імена savepoint-ів починаючи з версії 3.x.
Oracle:
-- Oracle savepoints:
-- - Можна reuse ім'я (на відміну від MySQL)
-- - Зберігаються в PGA (memory)
-- - Підтримують nested savepoints
Архітектурні Trade-offs
Підхід A: NESTED (savepoints)
- ✅ Плюси: Один connection, єдиний commit, partial rollback, lower overhead than REQUIRES_NEW
- ❌ Мінуси: Не працює з JTA, Hibernate L1 cache issues, savepoint limitations (MySQL auto-release), немає незалежного commit
- Підходить для: batch processing, bulk operations, compensation patterns всередині однієї БД
Підхід B: REQUIRES_NEW
- ✅ Плюси: Повна ізоляція, працює з JTA, незалежний commit, clean Hibernate semantics
- ❌ Мінуси: Два connection, suspend/resume overhead, ризик неузгодженості (inner committed, outer rolled back)
- Підходить для: audit, notification, cross-database operations
Підхід C: Manual savepoint management
@Transactional
public void manualSavepoint() {
Connection conn = DataSourceUtils.getConnection(dataSource);
Savepoint sp = conn.setSavepoint("my_sp");
try {
// business logic
} catch (Exception e) {
conn.rollback(sp); // Ручной rollback
}
// ...
}
- ✅ Плюси: Повний контроль, можна інтегрувати з будь-якою логікою
- ❌ Мінуси: Boilerplate, error-prone, bypass-ить Spring transaction management
- Підходить для: складних сценаріїв, де Spring propagation недостатній
Edge Cases та Corner Cases
1. MySQL Savepoint Auto-Release:
@Transactional
public void mysqlSavepointIssue() {
nestedService.doWork(); // NESTED → SAVEPOINT sp1
// Якщо всередині doWork() був rollback to sp1 → MySQL auto-release sp1
nestedService.doWork2(); // NESTED → намагається створити SAVEPOINT sp1 знову
// PostgreSQL: OK (savepoint можна reuse)
// MySQL: OK (нове ім'я sp2)
// Але якщо Spring reuse-ить те саме ім'я → MySQL ERROR:
// "Savepoint 'NESTED_SAVEPOINT_1' does not exist"
}
Spring вирішує це генерацією унікальних імен:
// DefaultTransactionStatus
private String generateSavepointName() {
return "NESTED_SAVEPOINT_" + System.identityHashCode(this);
}
2. Hibernate L1 Cache та Savepoint Rollback:
@Transactional
public void hibernateSavepointIssue() {
Entity e1 = new Entity("first");
em.persist(e1);
em.flush(); // INSERT в БД
try {
nestedService.failOperation(); // NESTED → кидає exception
} catch (Exception ex) {
// Savepoint rollback: БД скасувала INSERT
// АЛЕ: Hibernate L1 cache (PersistenceContext) все ще містить e1!
// e1.status = MANAGED, але в БД рядка немає
}
// При flush/commit:
// Hibernate може спробувати UPDATE e1 → ERROR (рядка немає)
// Або: Hibernate dirty checking → INSERT e1 знову → дублікат
}
Рішення:
@Transactional(propagation = Propagation.NESTED)
public void failOperation() {
Entity e = new Entity("will-fail");
em.persist(e);
em.flush();
// Після flush, якщо exception → savepoint rollback видалить INSERT з БД
// Але L1 cache все ще містить e → потрібен em.clear()
throw new RuntimeException("fail");
}
// Outer method:
@Transactional
public void safeNestedOperation() {
try {
nestedService.failOperation();
} catch (Exception e) {
em.clear(); // Очистити L1 cache від potentially rolled-back entities
}
}
Покроково: (1) em.persist() реєструє entity в PersistenceContext як MANAGED.
(2) em.flush() надсилає INSERT в БД. (3) Виняток тригерить savepoint rollback —
INSERT видаляється з БД. (4) Але PersistenceContext все ще містить entity зі
статусом MANAGED. (5) При фінальному commit Hibernate намагається знову flush → помилка.
em.clear() вирішує проблему, discarding весь PersistenceContext.
3. Nested Savepoints (savepoint всередині savepoint):
@Transactional
public void deeplyNested() {
level1Service.doWork(); // NESTED → SAVEPOINT sp1
try {
level2Service.doWork(); // NESTED → SAVEPOINT sp2 (всередині sp1)
} catch (Exception e) {
// Rollback to sp2 — sp1 все ще активний
}
try {
level3Service.doWork(); // NESTED → SAVEPOINT sp3
} catch (Exception e) {
// Rollback to sp3 — sp1 та sp2 (якщо не released) активні
}
}
// Ліміт: PostgreSQL — unlimited nested savepoints
// MySQL — unlimited, але кожен rollback to auto-releases
// Oracle — unlimited
4. Savepoint та Sequence (PostgreSQL):
BEGIN;
SAVEPOINT sp1;
SELECT nextval('my_seq'); -- поверне 1
ROLLBACK TO SAVEPOINT sp1;
-- Sequence value НЕ відкочується!
SELECT nextval('my_seq'); -- поверне 2, не 1
-- Це тому що sequence — не транзакційна структура
Impact: Якщо бізнес-логіка покладається на sequence values всередині nested транзакцій, будуть gaps.
5. Savepoint та Temporary Tables:
-- PostgreSQL: temporary tables НЕ rollback-яться до savepoint
BEGIN;
CREATE TEMP TABLE tmp_data (id INT);
SAVEPOINT sp1;
INSERT INTO tmp_data VALUES (1);
ROLLBACK TO SAVEPOINT sp1;
SELECT * FROM tmp_data; -- ПОРОЖНІЙ — rollback спрацював
-- АЛЕ: DROP TEMP TABLE не rollback-иться
SAVEPOINT sp2;
DROP TABLE tmp_data;
ROLLBACK TO SAVEPOINT sp2;
SELECT * FROM tmp_data; -- ERROR: table doesn't exist!
6. Savepoint та Triggers:
-- Якщо тригер викликає exception всередині nested транзакції:
CREATE TRIGGER order_before_insert
BEFORE INSERT ON orders
FOR EACH ROW
EXECUTE FUNCTION validate_order();
-- Якщо тригер падає:
-- Savepoint rollback скасовує INSERT
-- АЛЕ: side-effects тригера (наприклад, sequence nextval) не відкочуються
Performance Implications
| Операція | Latency | Throughput Impact |
|---|---|---|
| Savepoint creation | ~0.1ms | Negligible |
| Savepoint release | ~0.05ms | Negligible |
| Rollback to savepoint | ~0.5-2ms (залежить від кількості змін) | Низький |
| 10 nested savepoints | ~1-3ms total | -2-5% |
| 100 nested savepoints | ~10-30ms | -10-15% (undo log growth) |
Конкретні цифри (Spring Boot 3.x, PostgreSQL 15, HikariCP pool=50):
- REQUIRED only: ~25,000 TPS
- REQUIRED + 1 NESTED: ~24,000 TPS (savepoint overhead ~4%)
- REQUIRED + 10 NESTED: ~22,000 TPS (cumulative savepoint overhead)
- REQUIRED + 1 REQUIRES_NEW: ~20,000 TPS (suspend/resume ~20%)
Memory overhead:
- Savepoint object: ~500 bytes (JDBC) + DB-side state
- PostgreSQL: savepoint в shared memory, ~1KB per savepoint
- MySQL: savepoint в InnoDB trx struct, ~2KB per savepoint
- Undo log retention: при довгій транзакції з savepoints — dead tuples accumulate
Memory Implications
- JDBC Savepoint: ~500 bytes per savepoint object (client-side)
- DB-side savepoint state: 1-2KB per savepoint в shared memory
- Undo log / dead tuples: Кожна операція всередині savepoint створює undo records. При rollback — undo records не звільняються до основного commit.
- Hibernate L1 cache: Може містити stale entities після savepoint rollback — до кількох MB для великих операцій.
- Connection holder state: Spring зберігає savepoint reference в TransactionStatus — ~200 bytes per nested level.
Concurrency Aspects
NESTED concurrency model:
Thread-1: outerMethod(REQUIRED)
→ nestedService.doWork(NESTED) → savepoint sp1
→ Якщо exception: rollback to sp1 (тільки Thread-1 affected)
→ nestedService.doWork2(NESTED) → savepoint sp2
→ commit sp2 (release)
→ commit outer (commit all)
Thread-2: outerMethod(REQUIRED)
→ nestedService.doWork(NESTED) → savepoint sp3
Thread-1 та Thread-2 використовують різні connections → незалежні
Savepoints видимі тільки в рамках своєї транзакції
Savepoint та lock behavior:
-- Row locks, acquired всередині savepoint, release-яться при rollback to savepoint
BEGIN;
SAVEPOINT sp1;
UPDATE accounts SET balance = 100 WHERE id = 1; -- row lock acquired
ROLLBACK TO SAVEPOINT sp1;
-- Row lock released!
-- АЛЕ: DDL всередині savepoint (PostgreSQL):
SAVEPOINT sp2;
ALTER TABLE orders ADD COLUMN new_col TEXT; -- DDL
ROLLBACK TO SAVEPOINT sp2;
-- DDL НЕ відкочується до savepoint! (DDL implicit commit)
Real Production Scenario
Ситуація: Log aggregation platform (2024), Spring Boot 3.1, PostgreSQL, ingestion 50,000 events/sec.
Проблема: При batch insert логів (1000 events/batch), один malformed event викликав rollback всього batch. Throughput падав на 30% через повторної обробки.
Original code:
@Transactional
public void ingestBatch(List<LogEvent> events) {
for (LogEvent event : events) {
LogEvent validated = validate(event); // Може кинути ValidationException
logRepo.save(validated);
}
// Один ValidationException → rollback всього batch (1000 events lost)
}
Спроба №1: try-catch всередині REQUIRED
@Transactional
public void ingestBatch(List<LogEvent> events) {
for (LogEvent event : events) {
try {
LogEvent validated = validate(event);
logRepo.save(validated);
} catch (ValidationException e) {
failedCount++;
// Проблема: transaction marked rollback-only!
// При commit: UnexpectedRollingBackException
}
}
}
Фінальне рішення: NESTED
@Autowired private EventImportService eventImportService;
@Transactional
public void ingestBatch(List<LogEvent> events) {
int success = 0, failed = 0;
for (LogEvent event : events) {
try {
eventImportService.importEvent(event); // NESTED
success++;
} catch (ValidationException e) {
failed++;
// Тільки цей event відкотився до savepoint
}
}
metrics.record(success, failed);
// COMMIT — всі успішні events збережені
}
@Service
public class EventImportService {
@Transactional(propagation = Propagation.NESTED)
public void importEvent(LogEvent event) {
LogEvent validated = validate(event); // ValidationException → savepoint rollback
logRepo.save(validated);
// No exception → savepoint released
}
}
Результат:
- Batch completion rate: з 70% (30% rollback) до 99.5%
- Throughput: з 35,000 до 48,000 events/sec (+37%)
- Latency p99: з 200ms до 50ms (менше retries)
- Trade-off: ~3% overhead від savepoint management
Monitoring та Діагностика
Track savepoint creation and rollback:
@Component
public class SavepointMonitor implements TransactionSynchronization {
private final MeterRegistry registry;
@Override
public void beforeCommit(boolean readOnly) {
// Track savepoint usage
}
@Override
public void afterCompletion(int status) {
if (status == STATUS_ROLLED_BACK) {
Counter.builder("transaction.savepoint.rollback")
.increment();
}
}
}
// Register
TransactionSynchronizationManager.registerSynchronization(new SavepointMonitor(registry));
Hibernate L1 cache monitoring:
@PersistenceUnit
private EntityManagerFactory emf;
public void checkL1CacheSize() {
SessionFactoryImpl sf = emf.unwrap(SessionFactoryImpl.class);
for (Session s : sf.getSessions()) {
PersistenceContext pc = s.getPersistenceContext();
int entityCount = pc.getEntitiesByKey().size();
if (entityCount > THRESHOLD) {
log.warn("Large L1 cache after nested operations: {}", entityCount);
}
}
}
PostgreSQL savepoint monitoring:
-- Немає direct savepoint stats, але можна monitor через:
SELECT
xact_commit,
xact_rollback,
tup_inserted,
tup_updated,
tup_deleted
FROM pg_stat_database
WHERE datname = 'your_db';
-- High rollback rate + high insert rate → potential savepoint rollback pattern
Best Practices для Highload
- Лімітуйте глибину nesting — не більше 10-20 nested savepoints per transaction.
- Використовуйте NESTED тільки для partial failure tolerance — не для бізнес-логіки.
- Flush + Clear після nested rollback з Hibernate:
try { nestedService.doWork(); } catch (Exception e) { em.clear(); // Обов'язково після savepoint rollback } - Уникайте sequence залежностей всередині nested транзакцій — gaps неминучі.
- Monitor savepoint rollback rate — високий % = проблема в валідації.
- Для batch processing: chunk-based підхід з NESTED на chunk level:
// Chunk size 100, NESTED per chunk for (List<Event> chunk : Lists.partition(events, 100)) { try { importChunk(chunk); // NESTED } catch (Exception e) { // Тільки chunk відкотився } } - Connection pool: NESTED використовує 1 connection → не потребує збільшення pool size.
- Test з production-like даними: Savepoint behavior може відрізнятися з різним volume даних.
- Consider alternative для JTA: Якщо потрібна distributed transaction — використовуйте REQUIRES_NEW + saga pattern.
- Document savepoint semantics: Переконайтеся, що команда розуміє різницю між NESTED та REQUIRES_NEW.
🎯 Шпаргалка для інтерв’ю
Обов’язково знати:
- NESTED створює JDBC savepoint всередині існуючої транзакції — не нову транзакцію
- При rollback внутрішньої — відкат до savepoint, зовнішня продовжується
- При rollback зовнішньої — відкочує все, включаючи nested (savepoint відкочується)
- Один COMMIT в самому кінці — всі успішні nested operations зберігаються атомарно
- Не працює з JTA (fallback до REQUIRES_NEW) — тільки JDBC 3.0+ savepoints
- Hibernate L1 cache inconsistency після savepoint rollback — потрібен em.flush() + em.clear()
Часті уточнюючі запитання:
- Чим NESTED відрізняється від REQUIRES_NEW? — NESTED: один commit, savepoint; REQUIRES_NEW: незалежний commit, 2 connection
- Що станеться якщо немає зовнішньої транзакції? — NESTED поводиться як REQUIRED (створює нову)
- Чому Hibernate L1 cache ламається? — PersistenceContext не відкочується при savepoint rollback
- Коли використовувати NESTED? — Batch processing з partial failure tolerance, chunk-based operations
Червоні прапорці (НЕ говорити):
- “NESTED = REQUIRES_NEW” — принципово різні механізми (savepoint vs suspend/new tx)
- “NESTED працює з JTA” — JTA не підтримує savepoints
- “Inner data видна іншим транзакціям одразу” — тільки після загального commit
Пов’язані теми:
- [[13. Що таке Propagation в Spring]]
- [[15. В чому різниця між REQUIRED та REQUIRES_NEW]]
- [[18. Що таке rollback в транзакціях]]
- [[16. Що таке анотація @Transactional]]