› @Transactional
Аннотация @Transactional — пример использования Spring AOP (Aspect-Oriented Programming).
- Когда ты вешаешь
@Transactionalна метод (или класс), Spring не отдает вызывающему коду оригинальный объект твоего сервиса. Он создает Proxy (Обертку). - Когда кто-то вызывает твой метод, вызов попадает в Proxy.
- Логика Proxy (Transaction Interceptor):
- Берет
Connectionиз пула (HikariCP). - Отключает автокоммит:
connection.setAutoCommit(false). - Вызывает твой реальный метод бизнес-логики.
- Если метод завершился успешно ->
connection.commit(). - Если вылетело исключение ->
connection.rollback(). - Возвращает
Connectionобратно в пул.
- Берет
Важные ограничения (Частые вопросы на собесе):
- Self-invocation (Самовызов): Если метод
A()(без транзакции) вызывает методB()(с@Transactional) внутри одного и того же класса, транзакция НЕ откроется. Вызов идет черезthis, минуя Proxy-объект. - Модификатор доступа:
@Transactionalработает только наpublicметодах (при использовании стандартного Spring AOP на базе динамических прокси). - Checked Exceptions: По умолчанию Spring делает
rollbackтолько приRuntimeException(Unchecked) иError. Если вылетит проверяемое исключение (например,IOException), транзакция закоммитится! Чтобы изменить это, нужно писать:@Transactional(rollbackFor = Exception.class).
› Propagation
Что делать Spring’у, если метод с @Transactional вызывает другой метод, который тоже помечен @Transactional? За это отвечает параметр propagation.
Существует 7 уровней пропагации:
REQUIRED(По умолчанию):- Если текущей транзакции нет — создает новую.
- Если транзакция уже есть — присоединяется к ней (выполняется в рамках того же физического
Connection). - Нюанс: Если вложенный метод выбросит исключение, вся общая транзакция пометится как
rollback-only. Даже если внешний метод поймает исключение вtry-catch, при попытке коммита Spring выброситUnexpectedRollbackException.
REQUIRES_NEW:- Всегда создает новую транзакцию (берет новый
Connectionиз пула). - Если текущая транзакция уже существует, она приостанавливается (suspend), пока не завершится новая.
- Сценарий: Логирование действий пользователя в БД. Даже если основной бизнес-процесс упал с ошибкой (rollback), запись в лог должна сохраниться (commit).
- Всегда создает новую транзакцию (берет новый
SUPPORTS:- Если транзакция есть — выполняется в ней.
- Если транзакции нет — выполняется без транзакции (в режиме автокоммита).
NOT_SUPPORTED:- Выполняется без транзакции.
- Если транзакция есть, она приостанавливается на время выполнения метода.
MANDATORY:- Обязательно требует наличия транзакции. Если её нет — выбрасывает исключение
IllegalTransactionStateException.
- Обязательно требует наличия транзакции. Если её нет — выбрасывает исключение
NEVER:- Требует, чтобы транзакции не было. Если она есть — выбрасывает исключение.
NESTED(Вложенная):- Если транзакции нет — работает как
REQUIRED(создает новую). - Если транзакция есть — создает Savepoint (Точку сохранения) внутри текущей транзакции. Если вложенный метод падает, откатывается только до этого Savepoint’а, а внешняя транзакция может продолжить работу и успешно закоммититься. (Требует поддержки Savepoints со стороны JDBC драйвера).
- Если транзакции нет — работает как
› Isolation (Уровни изоляции в Spring)
Параметр isolation в @Transactional позволяет задать уровень изоляции для конкретной транзакции, переопределяя дефолтные настройки БД.
@Transactional(isolation = Isolation.READ_COMMITTED)@Transactional(isolation = Isolation.REPEATABLE_READ)@Transactional(isolation = Isolation.SERIALIZABLE)Isolation.DEFAULT— использует уровень изоляции, настроенный по умолчанию в самой СУБД (например,READ COMMITTEDдля PostgreSQL).
› Spring Data Repositories
Spring Data JPA предоставляет интерфейсы (CrudRepository, JpaRepository), которые автоматически реализуются фреймворком.
- Транзакционность по умолчанию: Все методы в
SimpleJpaRepository(дефолтная реализация) уже помечены аннотацией@Transactional.- Методы чтения (
findById,findAll) помечены@Transactional(readOnly = true). - Методы записи (
save,delete) помечены обычным@Transactional.
- Методы чтения (
- Поэтому, если ты вызываешь
repository.save()из контроллера (без сервиса), транзакция всё равно откроется и закроется на уровне самого репозитория.