Ответ
Аннотация @Transactional в Spring предоставляет декларативное управление транзакциями на основе AOP (Aspect-Oriented Programming). Она избавляет разработчика от написания шаблонного кода для открытия, коммита и отката транзакций.
Ключевые атрибуты и их поведение:
- propagation (распространение): Определяет, как должна вести себя транзакция при наличии уже существующей.
REQUIRED(по умолчанию): Использует текущую транзакцию или создаёт новую.REQUIRES_NEW: Всегда создаёт новую транзакцию, приостанавливая текущую.NESTED: Создаёт вложенную транзакцию внутри текущей.
- isolation (изоляция): Определяет уровень изоляции транзакции (например,
READ_COMMITTED). - readOnly: Указывает, что транзакция только для чтения (может оптимизировать работу).
- timeout: Время (в секундах), после которого транзакция будет откатана.
- rollbackFor / noRollbackFor: Указывает, для каких исключений выполнять или не выполнять откат.
Пример использования:
@Service
public class TransferService {
@Autowired
private AccountRepository repository;
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
rollbackFor = {InsufficientFundsException.class, DataAccessException.class})
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
Account from = repository.findById(fromId).orElseThrow();
Account to = repository.findById(toId).orElseThrow();
from.debit(amount); // Списание
to.credit(amount); // Зачисление
repository.save(from);
repository.save(to);
// При любом unchecked исключении или указанном в rollbackFor произойдёт автоматический rollback.
}
}
Важные нюансы:
- Проксирование:
@Transactionalработает через динамические прокси (или AspectJ), поэтому вызов метода@Transactionalизнутри того же класса (например, черезthis.internalMethod()) не будет перехвачен прокси, и транзакция не откроется. Нужно использовать инъекцию самого бина или@Transactionalна вызываемом публичном методе. - Тестирование: В интеграционных тестах можно использовать
@Transactionalдля автоматического отката изменений после каждого теста. - Менеджеры транзакций: Для работы требуется правильно настроенный
PlatformTransactionManager(например,DataSourceTransactionManagerдля JDBC).
Ответ 18+ 🔞
Так, слушай, смотри, я тут про эту вашу @Transactional в Spring расскажу, чтоб ты понял, насколько это офигенная штука, если не накосячить.
Представь, что каждый раз, когда ты пишешь в базу, тебе надо вручную открывать транзакцию, потом коммитить, а если что-то пошло не так — откатывать. Это же пиздец какой-то, да? Ну вот, чтобы не писать эту хуйню каждый раз, умные дядьки придумали эту аннотацию. Она, как волшебный плащ-невидимка, оборачивает твой метод и делает всю грязную работу сама. Магия, блядь, на основе AOP.
Смотри, какие у неё есть кнопки-переключатели (атрибуты):
-
propagation (распространение): Это про то, что делать, если транзакция уже есть.
REQUIRED(стоит по умолчанию): Если транзакция уже идёт — юзаем её. Нету — создаём новую. Самый частый сценарий, в рот меня чих-пых.REQUIRES_NEW: А вот это уже поинтереснее. Она говорит: «Похуй на текущую транзакцию, я создам свою, новую, а старую на паузу поставлю». Полезно, когда тебе надо что-то записать в лог, даже если основная операция откатится.NESTED: Создаёт вложенную транзакцию. Если в ней пиздец — откатится только она, а основная может жить дальше. Красота.
-
isolation (изоляция): Ну, это классика.
READ_COMMITTED,REPEATABLE_READи прочая хуйня. Чтоб фантомные чтения и прочие глюки не вылезали. -
readOnly: Ставишь
trueи намекаешь базе, что ты только читать будешь. Она иногда может оптимизироваться, но это не точно. -
timeout: Через сколько секунд твою долгую транзакцию пристрелят и откатят. Чтоб не висела, сука, вечность.
-
rollbackFor / noRollbackFor: А вот это важно! Говоришь, за какие исключения откатываться (
InsufficientFundsException.class), а за какие — нет. По умолчанию откат только на unchecked исключениях (RuntimeExceptionи его дети). АSQLException— это checked, так что если не указал — транзакция закоммитится, а потом будет пиздец. Запомни это, ебта!
Вот смотри, как это выглядит в коде:
@Service
public class TransferService {
@Autowired
private AccountRepository repository;
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.READ_COMMITTED,
rollbackFor = {InsufficientFundsException.class, DataAccessException.class})
public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
Account from = repository.findById(fromId).orElseThrow();
Account to = repository.findById(toId).orElseThrow();
from.debit(amount); // Списание
to.credit(amount); // Зачисление
repository.save(from);
repository.save(to);
// Если тут вылетит InsufficientFundsException или что-то из DataAccessException — всё откатится автоматом. Магия!
}
}
А теперь, блядь, главный подводный камень, про который все обоссываются:
Всё это работает через прокси. Это значит, что когда ты вызываешь метод transferMoney из другого бина — всё ок. Но если ты вызовешь его изнутри того же самого класса, например, через this.transferMoney(...) или из другого не-проксированного метода — то прокси-обёртка не сработает, и транзакция НЕ ОТКРОЕТСЯ. Вообще. Ни хуя.
Представь: ты внутри своего сервиса вызываешь приватный метод, а он без транзакции. Пиздец и разочарование. Решение? Или выносить в отдельный бин, или ставить @Transactional на публичный метод, который ты вызываешь.
И да, чтоб эта красота работала, нужен правильно настроенный менеджер транзакций (PlatformTransactionManager), типа DataSourceTransactionManager. Без него — просто аннотация на бумажке.
Вот так вот, коротко и с матом. Главное — не наебаться с вызовами внутри класса и не забыть про rollbackFor для checked исключений.