Питання 20 · Розділ 11

Як налаштувати rollback для checked винятків?

За замовчуванням Spring не відкочує транзакцію при checked exceptions (нащадки Exception, але не RuntimeException). Щоб це виправити, використовуйте параметр rollbackFor.

Мовні версії: English Russian Ukrainian

🟢 Junior Level

За замовчуванням Spring не відкочує транзакцію при checked exceptions (нащадки Exception, але не RuntimeException). Щоб це виправити, використовуйте параметр rollbackFor.

Найпростіший спосіб

@Transactional(rollbackFor = Exception.class)
public void businessMethod() throws MyCheckedException {
    // Тепер БУДЬ-ЯКИЙ виняток викличе rollback
}

// rollbackFor каже TransactionInterceptor: «цей виняток теж вважай // сигналом до відкату». Перевірка: якщо exception instanceof rollbackFor → setRollbackOnly().

Аналогія

Уявіть пожежну сигналізацію: за замовчуванням спрацьовує тільки на пожежу (RuntimeException). Checked exception — це витік газу, який теж потрібно обробити. rollbackFor каже системі: “будь-яка аварія = евакуація”.

Для конкретного винятку

@Transactional(rollbackFor = IOException.class)
public void readFile() throws IOException {
    // IOException викличе rollback
}

Для кількох винятків

@Transactional(rollbackFor = {IOException.class, MyBusinessException.class})
public void process() throws IOException, MyBusinessException {
    // Обидва винятки викличуть rollback
}

Золоте правило

Якщо ваш бізнес-виняток означає, що операція не може бути завершена успішно — він має викликати відкат. Використовуйте rollbackFor = Exception.class для безпеки.

Коли НЕ варто змінювати rollback-правила

  1. Якщо exception = сигнал про баг (NPE, IAE) — дефолтний rollback правильний
  2. Якщо немає clear правила в команді — різні rollbackFor в різних сервісах створюють хаос
  3. Якщо використовуєте @ControllerAdvice — нехай exception долетить до контролера, а там вирішите, що повертати клієнту

🟡 Middle Level

Чому це важливо

В реальних проєктах сервіси часто використовують кастомні бізнес-винятки. Якщо вони наслідуються від Exception (Checked), Spring за замовчуванням зробить COMMIT при їх виникненні. Це може залишити базу в неузгодженому стані.

// ПОГАНО — InsufficientFundsException extends Exception
@Transactional
public void withdraw(BigDecimal amount) throws InsufficientFundsException {
    if (balance.compareTo(amount) < 0) {
        throw new InsufficientFundsException();
    }
    balance = balance.subtract(amount);
    // Spring зробить COMMIT попри виняток!
}

Три підходи до рішення

1. rollbackFor (рекомендується для checked)

@Transactional(rollbackFor = Exception.class)
public void withdraw(BigDecimal amount) throws InsufficientFundsException {
    // Rollback для всіх винятків
}

2. Перейти на RuntimeException

// InsufficientFundsException тепер extends RuntimeException
@Transactional
public void withdraw(BigDecimal amount) {
    if (balance.compareTo(amount) < 0) {
        throw new InsufficientFundsException();  // Rollback автоматично
    }
}

3. Програмний rollback

@Transactional
public void withdraw(BigDecimal amount) {
    try {
        if (balance.compareTo(amount) < 0) {
            throw new InsufficientFundsException();
        }
    } catch (InsufficientFundsException e) {
        TransactionAspectSupport.currentTransactionStatus()
            .setRollbackOnly();
        throw e;
    }
}

Поширені помилки

Помилка Що відбувається Як виправити
Забути rollbackFor COMMIT при checked exception rollbackFor = Exception.class
Одрук в rollbackForClassName Правило мовчки не спрацьовує Використовувати rollbackFor з Class literal
Conflicting rollbackFor + noRollbackFor Непередбачувана поведінка Однозначні правила
Catch без setRollbackOnly() Exception пойманий → COMMIT Викликати setRollbackOnly() в catch

Порівняння підходів

Підхід Плюси Мінуси Коли використовувати
rollbackFor = Exception.class Просто, надійно Rollback навіть для “не-помилок” Стандартний підхід
rollbackFor = {SpecificException.class} Точний контроль Потрібно перелічити всі Коли деякі checked exception = не помилка
Перехід на RuntimeException Автоматичний rollback, менше boilerplate Вимагає рефакторинг exception hierarchy Новий проєкт або рефакторинг
Програмний setRollbackOnly() Умовний rollback Boilerplate, Spring coupling Складна бізнес-логіка

Коли НЕ використовувати rollback для checked винятків

Ситуація Чому Альтернатива
Бізнес-валідація Очікувана ситуація, дані валідні noRollbackFor = ValidationException.class
Graceful degradation Система має працювати Не exception, а result object
Partial success Частина даних коректна Обробити, commit, log error

🔴 Senior Level

rollbackFor Attribute Resolution

Spring резолвить rollback rules через priority chain:

// AbstractFallbackTransactionAttributeSource
protected TransactionAttribute determineTransactionAttribute(AnnotatedElement ae) {
    for (TransactionAnnotationParser parser : annotationParsers) {
        TransactionAttribute attr = parser.parseTransactionAnnotation(ae);
        if (attr != null) return attr;
    }
    return null;
}

// SpringTransactionAnnotationParser
public TransactionAttribute parseTransactionAnnotation(Transactional ann) {
    RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute();

    Class<? extends Throwable>[] rollbackFor = ann.rollbackFor();
    for (Class<? extends Throwable> ex : rollbackFor) {
        rbta.getRollbackRules().add(new RollbackRuleAttribute(ex));
    }

    String[] rollbackForClassName = ann.rollbackForClassName();
    for (String ex : rollbackForClassName) {
        rbta.getRollbackRules().add(new RollbackRuleAttribute(ex));
    }

    Class<? extends Throwable>[] noRollbackFor = ann.noRollbackFor();
    for (Class<? extends Throwable> ex : noRollbackFor) {
        rbta.getRollbackRules().add(new NoRollbackRuleAttribute(ex));
    }

    return rbta;
}

String-Based Rollback Rules — Hidden Danger

@Transactional(rollbackForClassName = {"MyBusinessException"})
public void process() { ... }

Spring резолвить class name в рантаймі:

public RollbackRuleAttribute(String exceptionPattern) {
    this.exceptionName = exceptionPattern;
    // NOT validated at startup — resolved lazily during exception handling
}

Risk: Одрук → правило мовчки не спрацює → немає rollback. Використовуйте rollbackFor з Class literal для type safety.

Combining rollbackFor та noRollbackFor

@Transactional(
    rollbackFor = Exception.class,
    noRollbackFor = {NotFoundException.class, ValidationException.class}
)
public void process() {
    // Exception → rollback
    // NotFoundException → NO rollback (commit despite exception)
    // ValidationException → NO rollback
}

Rule matching order:

  1. Найбільш специфічний збіг перемагає (deepest in hierarchy)
  2. При однаковій глибині — перше правило, що збіглося
  3. Default rules (RuntimeException, Error) перевіряються останніми

// noRollbackFor = «це RuntimeException, але НЕ відкочуй». Корисно для // очікуваних бізнес-винятків, що наслідуються від RuntimeException.

Програмний Rollback — Коли і Навіщо

Scenario 1: Checked exception без propagation

@Transactional
public void processFiles(List<File> files) {
    for (File file : files) {
        try {
            processFile(file);  // throws IOException
        } catch (IOException e) {
            log.error("File {} failed", file.getName());
            TransactionAspectSupport.currentTransactionStatus()
                .setRollbackOnly();
            break;  // Continue processing, but mark for rollback
        }
    }
}

Scenario 2: Умовний rollback

@Transactional
public void transferWithLimit(Long from, Long to, BigDecimal amount) {
    if (amount.compareTo(DAILY_LIMIT) > 0) {
        log.warn("Exceeds daily limit");
        TransactionAspectSupport.currentTransactionStatus()
            .setRollbackOnly();
        return;  // Method returns normally, but transaction rolls back
    }
    // ... transfer logic
}

Edge Cases (мінімум 3)

  1. String typo в rollbackForClassName: @Transactional(rollbackForClassName = "BusinesException") — silently ignored. Spring не валідує class name при ініціалізації.

  2. Conflicting rules: rollbackFor = Exception.class + noRollbackFor = RuntimeException.class. Специфічність визначає переможця. RuntimeException глибше → noRollbackFor перемагає → немає rollback для RuntimeException, є для checked.

  3. AOP ordering: Custom exception handler aspect може перехопити exception до TransactionInterceptor. Interceptor бачить normal return → COMMIT. Рішення: @Order для контролю порядку.

  4. Self-invocation bypass: Виклик this.method() всередині класу — proxy не бере участі, rollback rules не застосовуються взагалі. Немає транзакції = немає rollback.

  5. Async методи: @Async з checked exceptions — exception не прокидається в caller’s transaction context. Caller продовжує COMMIT. Future містить exception — потрібно future.get().

Global Rollback Configuration

@Configuration
public class TransactionConfig {

    @Bean
    public TransactionInterceptor transactionInterceptor(
            PlatformTransactionManager tm) {

        TransactionInterceptor interceptor = new TransactionInterceptor();
        interceptor.setTransactionManager(tm);

        RuleBasedTransactionAttribute defaultRule =
            new RuleBasedTransactionAttribute();
        defaultRule.setRollbackRules(
            List.of(new RollbackRuleAttribute(Exception.class)));

        NameMatchTransactionAttributeSource source =
            new NameMatchTransactionAttributeSource();
        source.addTransactionalMethod("*", defaultRule);

        interceptor.setTransactionAttributeSource(source);
        return interceptor;
    }
}

Migration Strategy: From Checked to Unchecked

// Step 1: Додати rollbackFor до існуючих методів
@Transactional(rollbackFor = Exception.class)
public void oldMethod() throws BusinessException { ... }

// Step 2: Змінити ієрархію exception
public class BusinessException extends RuntimeException { ... }  // Was extends Exception

// Step 3: Прибрати throws та rollbackFor
@Transactional
public void newMethod() { ... }  // Clean

// Step 4: Оновити callers (більше не потрібен try-catch)
service.newMethod();  // Замість: try { service.oldMethod(); } catch...

Performance Numbers

Операція Час
Rollback rule resolution (cached) ~0.5 μs
rollbackFor class matching ~0.1 μs
String-based resolution ~5-10 μs (class loading)
Actual DB rollback 1-5 ms (PostgreSQL), 5-50 ms (InnoDB)

Memory Implications

  • RollbackRuleAttribute об’єкти — ~100 bytes each, зберігаються в TransactionAttribute
  • String-based rules: Class завантаження в runtime — memory overhead якщо exception class великий
  • Exception stack traces — можуть бути великими. При frequent rollbacks = GC pressure.

Thread Safety

Rollback rules immutable — thread-safe. ThreadLocal state в TransactionSynchronizationManager — конкурентні виклики ізольовані.

Production War Story

Фінансовий сервіс: withdraw() кидав InsufficientFundsException extends Exception. @Transactional без rollbackFor. При insufficient funds exception — транзакція робила COMMIT, баланс списувався, але переказ не завершувався. Гроші пропали. Fix: rollbackFor = Exception.class + міграція на RuntimeException hierarchy.

Monitoring

Debug logging:

logging:
  level:
    org.springframework.transaction.interceptor: TRACE

Micrometer:

@Bean
public TransactionSynchronization transactionMetrics(MeterRegistry registry) {
    return new TransactionSynchronization() {
        @Override
        public void afterCompletion(int status) {
            if (status == STATUS_ROLLED_BACK) {
                registry.counter("transaction.rollback").increment();
            }
        }
    };
}

Highload Best Practices

  1. Baseline: rollbackFor = Exception.class на всіх write-методах. Без винятків.
  2. Мінімізація checked exceptions: Перевести бізнес-винятки на RuntimeException. Aligns with Spring philosophy.
  3. Global configuration: Використовувати TransactionInterceptor з глобальними rollback rules замість дублювання анотацій.
  4. Never use rollbackForClassName: String-based rules — silent failure risk. Use Class literals.
  5. Test rollback: Integration tests мають перевіряти що rollback дійсно відбувається для кожного exception type.
  6. Monitor rollback rate: Високий % rollback = проблема в валідації/бізнес-логіці.

🎯 Шпаргалка для інтерв’ю

Обов’язково знати:

  • rollbackFor = Exception.class — rollback для всіх винятків, рекомендований baseline
  • rollbackFor = {SpecificException.class} — точний контроль для конкретних типів
  • Програмний rollback: TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()
  • rollbackForClassName (string) — risky: одрук мовчки ігнорується, використовуйте Class literals
  • noRollbackFor виключає specific винятки з rollback (наприклад, NotFoundException)
  • Rule matching order: найбільш специфічний збіг (deepest in hierarchy) перемагає

Часті уточнюючі запитання:

  • Три підходи до checked exception rollback? — rollbackFor, перехід на RuntimeException, програмний setRollbackOnly
  • Чому rollbackForClassName небезпечний? — String resolution lazily, typo не валідується при старті
  • Коли використовувати програмний rollback? — Умовний rollback, complex business rules
  • Global rollback configuration — як? — TransactionInterceptor з RuleBasedTransactionAttribute

Червоні прапорці (НЕ говорити):

  • “rollbackForClassName = безпечний спосіб” — silent failure при одруці
  • “Catch exception = automatic rollback” — Spring не бачить caught exceptions
  • “Можна змішувати rollbackFor та noRollbackFor бездумно” — conflicting rules створюють непередбачувану поведінку

Пов’язані теми:

  • [[19. Які винятки за замовчуванням викликають rollback]]
  • [[18. Що таке rollback в транзакціях]]
  • [[16. Що таке анотація @Transactional]]