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

Что произойдет при ошибке во время выполнения @Transactional метода?

2.0 Middle🔥 231 комментариев
#Spring Framework#Базы данных и SQL

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

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

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

@Transactional: обработка ошибок и откат

Аннотация @Transactional — это механизм Spring Framework для управления транзакциями базы данных. Когда ошибка возникает во время выполнения метода, помеченного @Transactional, происходит откат транзакции (rollback) с соблюдением определённых правил.

1. Поведение при исключениях по умолчанию

Checked исключения (не вызывают откат по умолчанию)

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;
    
    // ❌ Проблема: checked exception НЕ вызывает откат
    @Transactional
    public void createOrder(Order order) throws IOException {
        orderRepository.save(order);
        
        // Даже если IOException выбросится, транзакция КОММИТИТСЯ
        writeToFile("order_" + order.getId() + ".txt");
    }
    
    private void writeToFile(String filename) throws IOException {
        throw new IOException("Ошибка при записи файла");
    }
}

// Результат:
// 1. Order сохранён в БД (COMMIT)
// 2. IOException выброшена в вызывающий код
// 3. Данные остаются в БД (несогласованное состояние!)

// ✅ Решение 1: явно указать rollbackFor
@Transactional(rollbackFor = IOException.class)
public void createOrder(Order order) throws IOException {
    orderRepository.save(order);
    writeToFile("order_" + order.getId() + ".txt"); // Вызовет ROLLBACK
}

// ✅ Решение 2: обернуть в unchecked exception
@Transactional
public void createOrder(Order order) {
    orderRepository.save(order);
    try {
        writeToFile("order_" + order.getId() + ".txt");
    } catch (IOException e) {
        throw new RuntimeException("Ошибка при создании заказа", e);
    }
}

Unchecked исключения (вызывают откат по умолчанию)

@Service
public class PaymentService {
    @Autowired
    private PaymentRepository paymentRepository;
    
    // ✅ Unchecked exception вызывает откат автоматически
    @Transactional
    public void processPayment(Payment payment) {
        paymentRepository.save(payment); // Save вставляет в БД
        
        // Ошибка! Это вызовет ROLLBACK всей транзакции
        if (payment.getAmount() > 10000) {
            throw new IllegalArgumentException("Сумма слишком большая");
        }
    }
}

// Результат:
// 1. RuntimeException выброшена
// 2. Все изменения в транзакции откатываются (ROLLBACK)
// 3. Payment не сохранена в БД
// 4. БД остаётся в консистентном состоянии

2. Откат транзакции (Rollback)

@Service
public class TransactionRollbackDemo {
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private AccountRepository accountRepository;
    
    // Полный откат при ошибке
    @Transactional
    public void transferMoney(User from, User to, BigDecimal amount) {
        // Шаг 1: уменьшить баланс from
        from.setBalance(from.getBalance().subtract(amount));
        userRepository.save(from); // BUFFERED в транзакции
        
        // Шаг 2: увеличить баланс to
        to.setBalance(to.getBalance().add(amount));
        userRepository.save(to); // BUFFERED в транзакции
        
        // Шаг 3: логирование может вызвать ошибку
        if (amount.compareTo(BigDecimal.valueOf(5000)) > 0) {
            throw new RuntimeException("Трансфер не прошёл (сумма слишком большая)");
            // Все 2 save() откатываются — деньги остаются на месте!
        }
        
        // Если ошибки нет — обе save() коммитятся
    }
}

// ✅ Пример с обработкой ошибок
@Service
public class TransactionWithErrorHandling {
    @Autowired
    private UserRepository userRepository;
    
    @Transactional
    public boolean transferMoney(User from, User to, BigDecimal amount) {
        try {
            from.setBalance(from.getBalance().subtract(amount));
            userRepository.save(from);
            
            to.setBalance(to.getBalance().add(amount));
            userRepository.save(to);
            
            return true; // Транзакция коммитится
        } catch (RuntimeException e) {
            // Транзакция откатывается автоматически
            // Но мы можем залогировать ошибку
            System.err.println("Ошибка трансфера: " + e.getMessage());
            return false;
        }
    }
}

3. Явное управление откатом

@Service
public class ManualRollback {
    @Autowired
    private UserRepository userRepository;
    
    @Transactional
    public void processUser(User user) {
        userRepository.save(user);
        
        // Получить текущую транзакцию
        TransactionStatus status = TransactionAspectSupport.currentTransactionStatus();
        
        if (shouldRollback(user)) {
            // Явно выполнить откат
            status.setRollbackOnly(); // Пометить транзакцию на откат
        }
    }
    
    private boolean shouldRollback(User user) {
        return user.getAge() < 18; // Пример условия
    }
}

// Результат:
// 1. User сохранён в памяти Spring
// 2. setRollbackOnly() пометит транзакцию
// 3. При выходе из метода ALL изменения откатываются
// 4. User не будет в БД

4. Обработка определённых исключений

@Service
public class SelectiveRollback {
    @Autowired
    private OrderRepository orderRepository;
    
    // Откатывать только на определённые исключения
    @Transactional(
        rollbackFor = {PaymentException.class, InventoryException.class},
        noRollbackFor = {ValidationException.class} // Эти ошибки не откатывают
    )
    public void createOrder(Order order) throws Exception {
        orderRepository.save(order);
        
        // Это вызовет ROLLBACK
        if (!order.isValid()) {
            throw new PaymentException("Платёж не прошёл");
        }
        
        // Это НЕ вызовет ROLLBACK (noRollbackFor)
        if (order.getItems().isEmpty()) {
            throw new ValidationException("Заказ пуст");
        }
    }
}

// Результат:
// - PaymentException → ROLLBACK (откат)
// - ValidationException → COMMIT (коммит)

5. Откат в случае ошибки БД

@Service
public class DatabaseErrorHandling {
    @Autowired
    private UserRepository userRepository;
    
    @Transactional
    public void createUser(User user) {
        userRepository.save(user);
        
        // Если поле имеет UNIQUE constraint
        User duplicate = new User();
        duplicate.setEmail(user.getEmail()); // Такой email уже существует
        userRepository.save(duplicate);
        // DataIntegrityViolationException → ROLLBACK
    }
}

// Spring поймает SQLIntegrityConstraintViolationException
// и откатит ВСЮ транзакцию, включая первый save()

6. Откат при вложенных транзакциях

@Service
public class NestedTransactions {
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private AuditRepository auditRepository;
    
    @Transactional
    public void updateUser(User user) {
        userRepository.save(user);
        
        try {
            logAudit(user);
        } catch (Exception e) {
            // Ошибка в логировании
            System.err.println("Ошибка логирования: " + e.getMessage());
        }
    }
    
    // Вложенная транзакция
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void logAudit(User user) {
        auditRepository.save(new AuditLog(user));
        throw new RuntimeException("Ошибка при логировании"); // Откатит ТОЛЬКО logAudit
    }
}

// Результат с REQUIRES_NEW:
// 1. updateUser открывает транзакцию
// 2. userRepository.save() выполняется
// 3. logAudit открывает НОВУЮ транзакцию
// 4. auditRepository.save() выполняется в новой транзакции
// 5. RuntimeException откатывает ТОЛЬКО новую транзакцию (logAudit)
// 6. updateUser коммитится (user сохранён)
// 7. logAudit откатывается (audit НЕ сохранён)

// При REQUIRED (по умолчанию):
// Обе транзакции откатывались бы вместе
@Transactional(propagation = Propagation.REQUIRED) // Использует существующую
public void logAudit(User user) {
    // При ошибке откатится ВСЯ цепь транзакций
}

7. Откат в многоэтапных операциях

@Service
public class MultiStepTransaction {
    @Autowired
    private OrderRepository orderRepository;
    
    @Autowired
    private InventoryRepository inventoryRepository;
    
    @Autowired
    private NotificationService notificationService;
    
    // Saga Pattern — управление откатом
    @Transactional
    public void createOrderWithCompensation(Order order) {
        try {
            // Шаг 1: Создать заказ
            orderRepository.save(order);
            
            // Шаг 2: Зарезервировать товары
            inventoryRepository.reserve(order.getItems());
            
            // Шаг 3: Отправить уведомление
            notificationService.sendConfirmation(order);
            
        } catch (Exception e) {
            // При ошибке on step 3: откатываются шаги 1 и 2
            // При ошибке БД: откатываются ВСЕ шаги
            throw new RuntimeException("Ошибка при создании заказа", e);
        }
    }
}

Итоговое резюме

ИсключениеПо умолчаниюРезультат
Unchecked (RuntimeException)ОткатROLLBACK всей транзакции
Checked (IOException, etc)КоммитCOMMIT, несмотря на ошибку
С rollbackForОткатROLLBACK для указанных
С noRollbackForКоммитCOMMIT, несмотря на ошибку
DataIntegrityViolationОткатROLLBACK из-за constraint
REQUIRES_NEWОткат только вложеннойВложенная откатывается

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

// ✅ Правильно
@Transactional(rollbackFor = Exception.class) // Откатывать на ВСЕ ошибки
public void processData(Data data) throws Exception {
    repository.save(data);
    performValidation(data); // Может выбросить Exception
}

// ✅ Правильно — использовать unchecked exceptions в бизнес-логике
@Transactional // По умолчанию откатывает unchecked exceptions
public void transferMoney(User from, User to, BigDecimal amount) {
    if (from.getBalance().compareTo(amount) < 0) {
        throw new InsufficientFundsException("Недостаточно средств");
    }
    // ...
}

// ❌ Неправильно — игнорировать ошибки
@Transactional
public void processOrder(Order order) {
    try {
        repository.save(order);
        performPayment(order);
    } catch (Exception e) {
        // Пустой catch — ошибка не откатит транзакцию
    }
}
Что произойдет при ошибке во время выполнения @Transactional метода? | PrepBro