Сьогодні наша мета — розібратися з винятками в контексті транзакцій. Навіщо це важливо? Бо збої трапляються, і треба вміти їх обробляти, щоб дані в вашій базі не перетворилися на хаос.
Винятки у світі транзакцій
Як програмісти, ми живемо у світі винятків. Вони чатують на нас буквально на кожному кроці: від NullPointerException при виклику методу на ні в чому не винному null до SQLException при помилках у SQL-запитах. Коли справа доходить до транзакцій, винятки стають ще більш підступними, бо помилка може означати, що дані в базі опиняться в неконсистентному стані.
Основні види винятків у транзакціях
У контексті транзакцій винятки діляться на два великі табори:
- Системні винятки. Це ті несподіванки, які ми не завжди можемо передбачити: наприклад, падіння підключення до бази даних (
JDBCException), помилки мережі або брак пам'яті в системі. Вони призводять до того, що транзакцію потрібно відкотити. - Прикладні винятки. Це ті помилки, які ми можемо і повинні обробляти: порушення бізнес-логіки, скажімо, перевищення ліміту на банківському рахунку або спроба зареєструвати користувача з вже існуючим email. Такі винятки можуть як вимагати відкату транзакції, так і ні — залежно від контексту.
Як транзакції працюють з винятками в Spring?
У Spring за автоматизацію управління транзакціями відповідає анотація @Transactional. І коли в межах методу, анотованого @Transactional, відбувається помилка, Spring бере ситуацію під контроль: він або відкотить транзакцію, або залишить зміни в базі, якщо правило таке.
Відкат транзакції
Зазвичай Spring автоматично відкотить транзакцію, якщо виникає необроблений виняток типу RuntimeException або Error. Це означає, що якщо ваш метод викидає, наприклад, NullPointerException або IllegalArgumentException, то всі зміни, зроблені в базі, будуть відкотені.
Однак checked-винятки (наприклад, SQLException) не спричиняють відкат за замовчуванням. Це пов'язано з тим, що Spring дотримується концепції, що checked-винятки повинні бути оброблені в коді вручну.
Приклади обробки винятків
Почнемо з базового прикладу. Припустимо, у нас є сервіс для роботи з банківськими рахунками. Ми хочемо списати гроші з одного рахунку і зарахувати їх на інший. У випадку помилки (наприклад, якщо на вихідному рахунку недостатньо коштів), транзакцію треба відкотити.
@Service
public class BankTransactionService {
@Autowired
private AccountRepository accountRepository;
@Transactional
public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
Account fromAccount = accountRepository.findById(fromAccountId)
.orElseThrow(() -> new RuntimeException("Рахунок відправника не знайдено"));
Account toAccount = accountRepository.findById(toAccountId)
.orElseThrow(() -> new RuntimeException("Рахунок отримувача не знайдено"));
if (fromAccount.getBalance().compareTo(amount) < 0) {
throw new RuntimeException("Недостатньо коштів на рахунку відправника");
}
fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
toAccount.setBalance(toAccount.getBalance().add(amount));
accountRepository.save(fromAccount);
accountRepository.save(toAccount);
}
}
У цьому прикладі, якщо баланс відправника менший за запитувану суму, метод викине RuntimeException, і всі зміни, внесені в базу, автоматично відкотяться.
Керування відкатом через rollbackFor
Якщо ви хочете, щоб checked-винятки (наприклад, SQLException) теж спричиняли відкат, потрібно явно вказати це за допомогою параметра rollbackFor. Ось приклад:
@Transactional(rollbackFor = {SQLException.class})
public void performDatabaseOperation() throws SQLException {
// Дії з базою даних
throw new SQLException("Помилка в базі даних");
}
Тепер, якщо виникне SQLException, транзакція буде відкотена.
Винятки, які не вимагають відкату
Іноді треба, щоб певні винятки не призводили до відкату транзакції, навіть якщо це RuntimeException. Для цього використовується параметр noRollbackFor:
@Transactional(noRollbackFor = {IllegalArgumentException.class})
public void processTransaction() {
// Обробка даних
throw new IllegalArgumentException("Цей виняток не спричинить відкат транзакції");
}
Як гарантувати відкат транзакції?
Відкат транзакції — це механізм захисту даних. Якщо метод викидає виняток, Spring гарантує відкат, але є нюанси:
- Не обгортайте транзакційні методи в
try-catch, якщо хочете відкат. Якщо ви ловите виняток і не викидаєте його знову, Spring вважає, що метод успішно завершився, і не відкотить транзакцію.@Transactional public void updateAccount() { try { // Дії з базою } catch (Exception e) { // Лог помилки, але виняток не викидається знову } }Тут транзакція не відкотиться, навіть якщо виникла помилка.
- Пам'ятайте про внутрішні виклики. Якщо транзакційний метод викликає зсередини інший транзакційний метод, Spring не завжди "побачить" виняток. Приклад неправильного виклику:
@Service public class MyService { @Transactional public void methodA() { methodB(); // Виклик відбувається всередині того ж класу } @Transactional public void methodB() { // Логіка throw new RuntimeException("Помилка!"); } }Тут транзакція не відкотиться, бо
methodB()викликалась напряму, а не через прокси-об'єкт Spring.
Обробка винятків і їх логування
Обробляти винятки не тільки корисно, але й потрібно. Треба принаймні логувати їх, інакше під час відладки ви витратите більше часу, ніж на саму розробку. Ось приклад:
@Autowired
private Logger logger;
@Transactional
public void deleteAccount(Long accountId) {
try {
accountRepository.deleteById(accountId);
} catch (DataAccessException e) {
logger.error("Не вдалося видалити запис з ID {}", accountId, e);
throw e; // або викидаємо виняток далі
}
}
Типові помилки при роботі з винятками в транзакціях
1. Ловити й ігнорувати винятки
Якщо ви обробляєте виняток і не сигналізуєте про помилку (не викидаєте виняток далі), транзакція не відкотиться. Це може призвести до неконсистентного стану даних.
2. Використовувати транзакції для читання
Якщо ви встановили @Transactional(readOnly = true) і при цьому вносите зміни в базу, це може призвести до несподіваних результатів. Наприклад, такі зміни можуть бути проігноровані.
Практичне застосування
Транзакції і їх обробка критично важливі при розробці корпоративного ПО. Уявіть собі інтернет-магазин: користувач додає товар у кошик, оформляє замовлення — і в цей момент транзакція має гарантувати, що товар зарезервований, а на складі зменшилася кількість доступних одиниць. Якщо щось піде не так — ніяких часткових операцій! Тільки відкат.
На співбесідах питання про обробку винятків і налаштування @Transactional зустрічаються постійно. Ваші знання допоможуть пояснити, як уникати помилок, і показати, що ви розумієте тонкощі роботи системи.
Не забувайте, головне правило транзакцій: або все, або нічого!
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ