← Назад к вопросам

Отменится ли транзакция при исключении в @Transactional методе

1.0 Junior🔥 291 комментариев
#Spring Framework

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

@Transactional и откат транзакции

Краткий ответ

Да, транзакция откатится, но ТОЛЬКО для проверяемых исключений (Checked Exception) нужна явная конфигурация. По умолчанию Spring откатывает транзакцию при непроверяемых исключениях (Runtime Exception), но при проверяемых исключениях (Checked Exception) совершает коммит.

Типы исключений в Java

// Unchecked Exception (Runtime Exception)
// Откатит транзакцию по умолчанию
throw new IllegalArgumentException("Invalid input");
throw new NullPointerException("Null value");
throw new RuntimeException("Runtime error");

// Checked Exception
// НЕ откатит транзакцию по умолчанию
throw new IOException("File not found");
throw new SQLException("DB error");

Иерархия исключений

Throwable
- Error (очень серьёзные ошибки)
- Exception
  - RuntimeException (Unchecked)
    - IllegalArgumentException
    - NullPointerException
  - Checked Exception (всё остальное)
    - IOException
    - SQLException

Поведение по умолчанию

Scenario 1: Unchecked Exception (откат по умолчанию)

@Service
public class UserService {
    @Transactional
    public void createUser(UserDto dto) {
        User user = new User();
        user.setName(dto.getName());
        userRepository.save(user);
        
        if (user.getName().length() < 3) {
            throw new IllegalArgumentException("Name too short");
        }
    }
}

Результат: транзакция ОТКАТИТСЯ

Scenario 2: Checked Exception (коммит по умолчанию)

@Service
public class FileService {
    @Transactional
    public void processFile() throws IOException {
        fileRepository.save(new File());
        throw new IOException("File not found");
    }
}

Результат: транзакция ЗАКОММИТИТСЯ (проблема!)

Правильная конфигурация

1. Откатывать Checked Exception

@Service
public class UserService {
    @Transactional(rollbackFor = {IOException.class, SQLException.class})
    public void createUserWithFile() throws IOException {
        User user = new User();
        userRepository.save(user);
        loadUserProfileFromFile();
    }
    
    private void loadUserProfileFromFile() throws IOException {
        throw new IOException("File not found");
    }
}

2. Не откатывать определённые Runtime Exception

@Service
public class PaymentService {
    @Transactional(noRollbackFor = {PaymentSkippedException.class})
    public void processPayment() {
        paymentRepository.save(payment);
        
        try {
            validateCard();
        } catch (PaymentSkippedException e) {
            log.warn("Payment skipped", e);
        }
    }
}

3. Обработка нескольких исключений

// Откатывать ВСЕ исключения
@Transactional(rollbackFor = Exception.class)
public void safeSave() {
    userRepository.save(user);
}

// Более селективно
@Transactional(rollbackFor = {IOException.class, SQLException.class},
               noRollbackFor = {ValidationException.class})
public void complexOperation() {
    // ...
}

Практический пример

@Service
public class OrderService {
    @Transactional(rollbackFor = PaymentException.class)
    public Order createOrder(OrderDto dto) throws PaymentException {
        Order order = new Order();
        order.setUserId(dto.getUserId());
        order.setAmount(dto.getAmount());
        orderRepository.save(order);
        
        try {
            paymentService.processPayment(order.getId(), order.getAmount());
        } catch (PaymentException e) {
            throw e;
        }
        
        try {
            emailService.sendConfirmation(order);
        } catch (EmailException e) {
            log.error("Email failed", e);
        }
        
        return order;
    }
}

Ловушки

Ловушка 1: Перехват исключения внутри метода

// Плохо: исключение поймано, транзакция не откатится
@Transactional
public void createUser() {
    User user = new User();
    userRepository.save(user);
    
    try {
        validateUser(user);
    } catch (RuntimeException e) {
        log.error("Validation failed", e);
        // Exception поймана, транзакция коммитится!
    }
}

// Хорошо: пробросить исключение дальше
@Transactional
public void createUser() {
    User user = new User();
    userRepository.save(user);
    validateUser(user);
}

Ловушка 2: Разные классы Exception

// Забыл IOException в rollbackFor
@Transactional(rollbackFor = SQLException.class)
public void process() throws IOException {
    // IOException не откатит транзакцию!
}

// Используй Exception как родительский класс
@Transactional(rollbackFor = Exception.class)
public void process() throws Exception {
    // Любое исключение откатит
}

Ловушка 3: @Transactional на приватных методах

// Не работает! @Transactional игнорируется на private
@Service
public class Service {
    @Transactional
    private void privateMethod() {  // НЕ будет работать
        // ...
    }
}

// Только public методы
@Service
public class Service {
    @Transactional
    public void publicMethod() {  // работает
        // ...
    }
}

Лучшие практики

  1. Используй unchecked exception для бизнес-ошибок

    • Создавай свои RuntimeException подклассы
    • Пробрасывай их вверх по стеку
  2. Явно указывай rollbackFor для checked exception

    • @Transactional(rollbackFor = IOException.class)
  3. Разделяй операции с разными требованиями

    • Основная операция с @Transactional
    • Побочные операции (email, логирование) без аннотации
  4. Не лови исключение внутри @Transactional

    • Если нужен откат, пробрось исключение наружу
  5. Используй readOnly=true для чтения

    • @Transactional(readOnly = true)

Таблица поведения

ИсключениеТипrollbackForРезультат
RuntimeExceptionUnchecked-Откат
IOExceptionChecked-Коммит
IOExceptionCheckedIOExceptionОткат
IllegalArgumentExceptionUncheckedExceptionОткат

Вывод

Spring @Transactional откатывает только Runtime Exception по умолчанию. Для Checked Exception нужно явно указать rollbackFor. Всегда проверяй типы исключений и не лови их внутри метода, если нужен откат.