В прошлой лекции мы разобрались с типами исключений в транзакциях и базовыми механизмами их обработки в Spring. Теперь пришло время углубиться в практику и разобрать более сложные сценарии использования rollback.
Почему важно правильно настраивать rollback?
Неправильная настройка откатов может привести к серьезным проблемам:
- Незавершённые изменения: часть данных может остаться в неконсистентном состоянии, например, при переводе денег между счетами.
- Нарушение бизнес-правил: система может сохранить изменения, которые противоречат бизнес-логике (скажем, заказ с превышенным лимитом).
- Проблемы масштабирования: в многопользовательских системах неверная обработка транзакций приводит к сложно отслеживаемым ошибкам.
Spring предоставляет гибкие инструменты для управления откатом транзакций. Давайте научимся их использовать.
Практические сценарии использования rollback
В реальных приложениях часто встречаются ситуации, когда стандартного поведения транзакций недостаточно. Рассмотрим на примере интернет-магазина, как можно гибко управлять транзакциями в разных ситуациях.
@Service
public class OrderService {
@Autowired
private OrderRepository orderRepository;
@Autowired
private ProductService productService;
@Transactional
public void processOrder(Order order) {
// Проверяем наличие товара
if (!productService.isAvailable(order.getProductId())) {
// Программный откат: явно указываем Spring откатить транзакцию
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
throw new ProductNotAvailableException("Товар недоступен");
}
// Сохраняем заказ
orderRepository.save(order);
// Проверяем лимиты пользователя
if (orderRepository.getUserOrderCount(order.getUserId()) > 5) {
// Автоматический откат: Spring сам откатит транзакцию при исключении
throw new BusinessException("Превышен лимит заказов");
}
// Уменьшаем количество товара и обрабатываем особые случаи
try {
productService.decreaseStock(order.getProductId(), order.getQuantity());
} catch (StockWarningException e) {
// Логируем предупреждение, но позволяем транзакции завершиться
notifyManager(e);
}
}
// Пример, где некоторые ошибки не должны отменять всю операцию
@Transactional(noRollbackFor = {StockWarningException.class})
public void processOrderWithPartialErrors(Order order) {
orderRepository.save(order);
productService.decreaseStock(order.getProductId(), order.getQuantity());
// Даже если возникнет StockWarningException,
// заказ останется сохраненным
}
}
В этом примере мы видим три разных подхода к управлению транзакциями:
- Программный откат через
setRollbackOnly()— когда нам нужен полный контроль над процессом - Автоматический откат при исключении — стандартное поведение Spring для RuntimeException
- Выборочный откат с помощью
noRollbackFor— когда некоторые ошибки не должны отменять всю операцию
Каждый подход имеет свои преимущества:
- Программный откат даёт максимальный контроль
- Автоматический откат делает код чище и понятнее
- Выборочный откат позволяет гибко обрабатывать разные типы ошибок
Тестирование rollback
Для проверки правильности работы транзакций Spring предоставляет удобные инструменты тестирования. Давайте рассмотрим, как можно протестировать нашу логику обработки заказов:
@SpringBootTest
public class OrderServiceTest {
@Autowired
private OrderService orderService;
@Autowired
private OrderRepository orderRepository;
@Test
@Transactional
public void testOrderRollbackOnLimitExceeded() {
// Подготовка
Order order = new Order("Product A", 1);
// Действие
assertThrows(BusinessException.class, () -> {
orderService.processOrder(order);
});
// Проверка
assertEquals(0, orderRepository.count(),
"База данных должна быть пустой после отката транзакции");
}
@Test
@Transactional
public void testPartialRollback() {
Order order = new Order("Product B", 1);
// Этот метод не откатывает транзакцию при StockWarningException
orderService.processOrderWithPartialErrors(order);
// Проверяем, что заказ сохранился, несмотря на возможные предупреждения
assertTrue(orderRepository.existsById(order.getId()));
}
}
Обратите внимание:
- Аннотация
@Transactionalв тестах автоматически откатывает все изменения после каждого теста - Это позволяет нам не беспокоиться о очистке базы данных между тестами
- Мы можем легко проверить, как работает откат при различных сценариях
Типичные ошибки при работе с rollback
Работа с rollback может быть как благом, так и источником проблем, если не соблюдать осторожность:
1. Перехват исключений вручную: Если вы перехватываете исключение внутри транзакционного метода, Spring не узнает об ошибке, и транзакция завершится успешно.
Пример:
@Transactional
public void incorrectTransactionHandling() {
try {
saveDataToDatabase();
throw new RuntimeException("Ошибка");
} catch (Exception e) {
// Исключение перехвачено, транзакция не откатится!
}
}
2. Запуск транзакционного метода через this: Spring использует прокси для обработки транзакций. Если вы вызываете транзакционный метод из другого метода того же класса через this, аннотация @Transactional игнорируется.
public class OrderService {
@Transactional
public void methodA() {
// Транзакция будет работать
}
public void methodB() {
this.methodA(); // Транзакция не будет работать
}
}
Решение: инжектируйте сервис в самом себе или выносите вызов в другой класс.
3. Операции вне транзакции: если вы меняете данные вне транзакции, они не откатятся. Например, вызов внешнего API или изменение статических переменных остаются за рамками транзакции.
Заключение
Откат транзакций — мощный механизм в Spring, который позволяет обеспечить согласованность данных в случае сбоев или бизнес-ошибок. Но, как и с любым инструментом, откатами нужно пользоваться с умом. Надеюсь, вы больше не будете бояться слова rollback — это не ошибка, это спасательный круг, когда что-то пошло не так.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ