22 февраля 2020
Тэги: Java, Spring, Spring Data.
Транзакцией называется набор связанных операций, все из которых должны быть выполнены корректно без ошибок. Если при выполнении одной из операций возникла ошибка, все остальные должны быть отменены. Прежде всего такой механизм применяется при работе с БД.
Spring предлагает очень простой декларативный способ управления транзакциями. Вам достаточно добавить @org.springframework.transaction.annotation.Transactional к публичному сервисному методу, и все операции внутри этого метода будут выполняться в транзакции. При выходе из метода транзакция будет завершена (операция commit в терминах БД) автоматически. Если в процессе работы возникнет исключение и оно не будет перехвачено внутри метода, транзакция будет отменена (операция rollback) и все данные вернуться в то состояние, в котором они были до начала транзакции.
Предположим, у нас есть метод в спринговом сервисе, который выполняет несколько запросов в БД. Для простоты можно использовать Spring Data, чтобы оперировать записями в БД в ООП стиле.
Сама сущность, которую мы сохраняем в базу, имеет следующий вид:
Слой dao, представляющий базовые методы для сохранения данной сущности в БД выглядит так:
Теперь создадим метод в сервисном слое, который сначала создаёт сущность, сохраняет её в БД, затем обновляет её имя. То есть сначала в БД происходит запрос insert, затем update.
Сервисный метод вызывается из контроллера при помощи POST-запроса:
Данный код всегда работает без ошибок. А теперь добавим между созданием и обновлением записи в БД ошибку.
Если мы выполним данный код, то увидим, что новая запись была добавлена в таблицу, но до обновления дело уже не дошло. Чтобы держать наши данные согласованными, достаточно добавить к методу вышеупомянутую аннотацию:
В данном случае первый запрос будет выполнен, а потом отменён и в таблице в БД новых записей так и не появится.
Бывают такие ошибки, при которых откатывать транзакцию не требуется. Эти исключения вы можете перечислить при помощи параметра dontRollbackOn:
Если мы сделаем так для нашего примера, то запись всё-таки будет создана, затем возникнет исключение, обновления уже не произойдёт, но транзакция всё равно будет завершена.
Как Spring реализует механизм создания транзакций? Важно отметить, что @Transactional будет работать только для публичных методов, вызываемых из других компонентов. Это происходит потому что спринг делает вызов целевого метода через прокси-объект, имеющий такой же интерфейс, как и наш сервис. В этом прокси-объекте происходит открытие транзакции перед вызовом целевого метода. Затем, после завершения целевого метода, происходит закрытие транзакции. Если из метода вылетит исключение, транзакция будет отменена. Это поведение похоже на конструкцию try-catch. Можно представлять себе логику работы прокси так:
Если вы повесите @Transactional на какой-либо метод и вызовете его из того же сервиса, но из другого метода, механизм работать не будет, т.к. вызов не проходит через прокси. Это надо иметь в виду.
Kotlin, Java, Spring, Spring Boot, Spring Data, SQL, PostgreSQL, Oracle, Linux, Hibernate, Collections, Stream API, многопоточность, файлы, Nginx, Apache, maven, gradle, JUnit, YouTube, новости, руководство, ООП, алгоритмы, головоломки, rest, GraphQL, Excel, XML, json, yaml.
29.06.2021 21:28 Царский Евгений
Хотелось бы прояснить последний момент: "Если вы повесите @Transactional на какой-либо метод и вызовете его из того же сервиса, но из другого метода, механизм работать не будет, т.к. вызов не проходит через прокси". Что-то я тут не понял, ну так мы вызываем методы сервиса из контроллера, методы в контроллере не аннотированы @Transactional, а транзакция открывается. Или имеется в виду, что транзакция не будет открыта в том методе, который не аннотирован, даже если внутри него вызвать аннотированный метод (разумеется, сам аннотированный метод для себя откроет свою транзакцию)?
29.06.2021 22:17 devmark
Речь тут идёт о том, что аннотация @Transactional на методе будет проигнорирована, если этот метод вызвать из того же класса, где он находится. Для работы транзакции нужно обязательно делать вызов из другого бина. Что мы и делаем, вызывая сервисный метод из контроллера.
20.09.2021 11:08 Александр
Идеально подана информация. Кратко и по делу. Спасибо!
13.06.2022 13:04 Евгений
Действительно все понятно, без лишней воды, спасибо!
18.07.2022 19:45 Владислав
Даа, именно эта аналогия с Try - catch далма мне понимание того, что здесь происходит, спасибо!)