Ответ
Аннотация @Transactional в Spring декларативно управляет границами транзакций для методов или классов. При вызове аннотированного метода Spring создает или присоединяется к существующей транзакции, а по завершении — фиксирует (commit) или откатывает (rollback) изменения.
Основной принцип:
- Перед методом: Открывается транзакция (или используется существующая).
- Во время метода: Все операции с БД выполняются в этой транзакции.
- После метода: Если метод завершился успешно (без исключения) — транзакция фиксируется. Если было выброшено исключение — транзакция откатывается.
Ключевые параметры аннотации:
propagation— определяет поведение при вложенных вызовах. Например:REQUIRED(по умолчанию): использует текущую транзакцию или создает новую.REQUIRES_NEW: всегда создает новую транзакцию, приостанавливая текущую.
isolation— уровень изоляции транзакции (например,READ_COMMITTED).rollbackFor/noRollbackFor— указывает, для каких исключений делать или не делать откат.readOnly—trueдля оптимизации операций только на чтение.timeout— таймаут транзакции в секундах.
Пример использования:
@Service
public class TransferService {
@Autowired
private AccountRepository accountRepository;
@Transactional(rollbackFor = {InsufficientFundsException.class})
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
if (from.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException("Недостаточно средств");
}
from.debit(amount); // Списываем
to.credit(amount); // Зачисляем
// Оба вызова save() выполняются в одной транзакции.
// При исключении балансы не изменятся.
accountRepository.save(from);
accountRepository.save(to);
}
}
Важные ограничения:
- Работает только на public-методах из-за механизма проксирования AOP.
- По умолчанию откатывает транзакцию только для unchecked (
RuntimeException) иErrorисключений. Для checked-исключений отката не будет. - Самовызов метода внутри одного класса (без прокси) не запустит транзакционное поведение.
Ответ 18+ 🔞
Давай разберём эту вашу транзакционную магию, а то народ путается, как хуй в проруби.
Вот смотри, есть у тебя метод, который должен делать несколько операций с базой. Например, перевести бабки с одного счёта на другой. Если первую операцию выполнил, а на второй — пиздец, исключение вылетело, то получается хуйня: деньги уже списали, а на другой счёт не зачислили. Бабки просто испарились, как твоя зарплата в пятницу вечером.
Чтобы такого не было, умные дядьки придумали транзакции. А в Spring за её управление отвечает аннотация @Transactional. Она, блядь, как надёжный друг: либо всё делает как надо, либо, если что-то пошло не так, откатывает всё к хуям, будто ничего и не было.
Как это, сука, работает?
- Заходишь в метод — Spring открывает транзакцию (или цепляется к уже открытой, если она есть).
- Выполняешь свои штуки — все запросы к базе внутри этой транзакции.
- Выходишь из метода — если вышел красиво, без скандала, Spring говорит базе: «Всё, чувак, фиксируй (commit), я доволен». Если же вылетело исключение — Spring орёт: «Откатывай всё нахуй (rollback), тут пиздец случился!».
На что можно смотреть в этой аннотации?
propagation— решает, что делать, когда транзакция встречает другую транзакцию. Как в подъезде два мужика. По умолчанию стоитREQUIRED: «Брат, если ты уже в процессе, давай вместе, а если нет — я начну». А естьREQUIRES_NEW— это такой похуист: «Отъебись, я свою заведу, а твою на паузу поставлю».isolation— уровень изоляции. Чтоб другие транзакции не мешали, как назойливые соседи.READ_COMMITTED— стандартный, чтобы не видеть не зафиксированный другими говнокод.rollbackFor/noRollbackFor— тут указываешь, от каких исключений откатываться, а от каких — нет. По умолчанию откатывается отRuntimeExceptionиError. А если твой метод кидает какое-нибудь своёSuperImportantCheckedException, и откатываться от него не надо — указываешь вnoRollbackFor.readOnly— ставишьtrue, если метод только читает. Это как табличку «руками не трогать» для базы, может немного ускорить.timeout— время в секундах, сколько транзакция может мучать базу. Если не успела — ей кирдык.
Вот живой пример, чтобы не быть пиздаболом:
@Service
public class TransferService {
@Autowired
private AccountRepository accountRepository;
@Transactional(rollbackFor = {InsufficientFundsException.class})
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
Account from = accountRepository.findById(fromId).orElseThrow();
Account to = accountRepository.findById(toId).orElseThrow();
if (from.getBalance().compareTo(amount) < 0) {
throw new InsufficientFundsException("Недостаточно средств");
}
from.debit(amount); // Списываем
to.credit(amount); // Зачисляем
// Оба вызова save() в одной транзакции.
// Кинет исключение — ни хуя не спишется.
accountRepository.save(from);
accountRepository.save(to);
}
}
А теперь, блядь, главные подводные ебланы:
- Работает только на public-методах. Потому что Spring накидывает свою магию (прокси) поверх класса, а приватные методы ему не видны. Вызовишь изнутри класса — транзакции не будет, будет просто вызов метода. Пиздец, да?
- По умолчанию откатывает только на unchecked исключениях (
RuntimeException) иError. Если твой метод объявляет в сигнатуреthrows SQLException(checked), и оно вылетит — транзакция закоммитится, а деньги твои сгорят. Поэтому для таких случаев явно указывайrollbackFor = SQLException.class. - Самовызов внутри класса не работает. Если метод
a()вызывает методb()с@Transactionalвнутри одного и того же бина — транзакция дляb()не откроется. Потому что Spring-прокси тут не при чём, это прямой вызов. Решение — выносиb()в отдельный бин или используйAopContext.currentProxy(), но это уже, прости господи, извращение.
Короче, инструмент мощный, но если не понимать, как он устроен внутри, можно наломать таких дров, что потом пол-проекта переписывать. Держи ухо востро.