Уявіть собі ситуацію: потрібно реалізувати механізм переказу грошей між двома банківськими рахунками. Очевидно, що операція складається з двох кроків:
- Списання коштів з одного рахунку.
- Зарахування коштів на інший рахунок.
Якщо в процесі щось піде не так, наприклад, збій на другому кроці, важливо відкотити всю операцію, щоб уникнути ситуації, коли гроші "зникли". Тут у гру вступає анотація @Transactional, дозволяючи вам зосередитися на бізнес-логіці, а сам Spring бере на себе управління транзакцією, роблячи код чистішим і простішим.
Як працює @Transactional?
@Transactional — це анотація, яка задає поведінку транзакції для методу або класу. Spring використовує аспектно-орієнтоване програмування (AOP), щоб обгорнути ваш код у "транзакційні проксі". Це означає, що коли метод запускається, Spring автоматично починає транзакцію, а по завершенні методу — або фіксує зміни (commit), або відкотить їх (rollback), якщо сталося виключення.
Ось приклад використання @Transactional для операції переказу грошей між рахунками:
@Service
public class BankService {
@Transactional
public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
// Крок 1: Списання коштів з одного рахунку
accountRepository.debit(fromAccountId, amount);
// Крок 2: Зарахування коштів на інший рахунок
accountRepository.credit(toAccountId, amount);
}
}
Просто? Але це лише верхівка айсберга.
Опції анотації @Transactional
Анотація @Transactional надає багато опцій для керування поведінкою транзакцій. Давайте розберемо найважливіші.
1. Propagation (поширення)
Це вказує, як має виконуватися транзакція, якщо метод викликається всередині іншої транзакції. Наприклад:
REQUIRED(за замовчуванням): Використовує існуючу транзакцію, якщо вона є. Якщо ні, створює нову.REQUIRES_NEW: завжди створює нову транзакцію, призупиняючи поточну.MANDATORY: вимагає існуючої транзакції. Якщо її немає – кинe виключення.SUPPORTS: працює в рамках транзакції, якщо вона є, але може бути викликаний і без неї.
Приклад використання:
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveAuditLog(AuditLog log) {
auditLogRepository.save(log);
}
2. Isolation (рівень ізоляції)
Вказує, як транзакція повинна бачити зміни, зроблені іншими транзакціями. Це важливо для роботи в мультикористувацьких системах.
Ось основні рівні ізоляції:
READ_UNCOMMITTED: транзакція може бачити непідтверджені зміни інших транзакцій (dirty read).READ_COMMITTED: виключає dirty read.REPEATABLE_READ: захист від non-repeatable reads (consistent reads).SERIALIZABLE: повна ізоляція, максимальний захист від конфліктів, але зі зниженням продуктивності.
Приклад:
@Transactional(isolation = Isolation.SERIALIZABLE)
public void performSensitiveOperation() {
// Критично важлива операція
}
3. Timeout (час виконання)
Іноді транзакції можуть блокуватися або працювати надто довго. Ви можете задати ліміт часу (у секундах), після якого транзакція буде перервана.
Приклад:
@Transactional(timeout = 5) // Максимум 5 секунд
public void executeLongQuery() {
// Довга операція
}
4. ReadOnly (тільки для читання)
Якщо транзакція тільки читає дані (без змін), ви можете вказати readOnly=true. Це може оптимізувати продуктивність.
Приклад:
@Transactional(readOnly = true)
public List<Account> getAllAccounts() {
return accountRepository.findAll();
}
5. RollbackFor (відкат транзакції при помилці)
За замовчуванням Spring відкачує транзакцію лише у випадку RuntimeException або Error. Якщо ви хочете відкотити транзакцію при інших виключеннях (наприклад, Exception), потрібно вказати це явно:
@Transactional(rollbackFor = Exception.class)
public void processTransaction() throws Exception {
// Код, який може кинути виключення
}
Реальні приклади використання
Приклад 1: Обробка замовлень в інтернет-магазині
Уявіть обробку замовлення, яка включає:
- Списання товару зі складу
- Зняття коштів з картки клієнта
- Запис даних про замовлення
@Service
public class OrderService {
@Transactional
public void processOrder(Order order) {
// Перевірка наявності товару
stockService.decreaseStock(order.getProductId(), order.getQuantity());
// Списання коштів
paymentService.charge(order.getPaymentInfo());
// Збереження замовлення
orderRepository.save(order);
}
}
Якщо на будь-якому етапі станеться помилка, всі зміни будуть відкоті.
Приклад 2: Логування помилок
Іноді потрібно зберігати журнал подій поза основною транзакцією. Використаємо REQUIRES_NEW:
@Service
public class LogService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void logError(String message) {
errorLogRepository.save(new ErrorLog(message));
}
}
Типові помилки при роботі з @Transactional
- Анотація не спрацювала. Це може статися, якщо метод викликано з того ж класу (через
this). Пам'ятайте, що Spring застосовує проксі, і виклик зсередини одного класу оминає механізм AOP. Рішення? Виносьте метод в інший@Service. - Поєднання зовнішніх і внутрішніх транзакцій. Наприклад, використання
@Transactional(propagation = Propagation.REQUIRES_NEW)всередині методу може створювати накладні витрати. Використовуйте тільки за потреби. - Неправильна поведінка rollback. Якщо ви забули вказати
rollbackFor, Spring може не відкотити транзакцію. Завжди явно вказуйте потрібну поведінку.
Практика: Впровадження @Transactional в додаток
Створимо метод для обробки замовлення з автоматичним відкатом транзакції у випадку помилки:
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
@Autowired
private OrderRepository orderRepository;
@Transactional
public void placeOrder(Long productId, int quantity) {
// Зменшуємо кількість товару
Product product = productRepository.findById(productId)
.orElseThrow(() -> new RuntimeException("Продукт не знайдено!"));
if (product.getStock() < quantity) {
throw new RuntimeException("Не вистачає товару на складі!");
}
product.setStock(product.getStock() - quantity);
productRepository.save(product);
// Зберігаємо замовлення
Order order = new Order();
order.setProductId(productId);
order.setQuantity(quantity);
orderRepository.save(order);
// Штучний збій для перевірки rollback
if (quantity > 10) {
throw new RuntimeException("Збій при обробці замовлення!");
}
}
}
Перевірка:
- Виклик методу
placeOrderз коректними даними — створить замовлення і зменшить кількість товару. - Запуск з великою кількістю (
quantity > 10) — викличе збій. Spring відкотить зміни, залишивши дані консистентними.
@Transactional — потужний інструмент, який знімає з нас головний біль управління транзакціями вручну. Використовуйте її відповідально, оптимізуйте параметри і завжди перевіряйте роботу через тести!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ