Декларативное управление транзакциями в Spring

Транзакция — это очень важное понятие в мире баз данных (и не только). Каждый, кто работал с базами данных, слышал про ACID и транзакции. Если кто не работал и не знает что это такое, то всё просто: транзакция это набор операций, которые могут быть либо целиком и успешно выполнены, либо полностью не выполнены.

Управление транзакциями поддерживается и в JDBC, и в JPA, и в Hibernate. И везде управление реализовано примерно одинаково:

  1. Открываем транзакцию
  2. Проводим какие-то действия над данными
  3. Подтверждаем, либо откатываем транзакцию

Например, код сохранения объекта в может выглядеть так:

На четыре строки кода только одна решает проблему разработчика, а все остальные нужны только для управления persistence context и транзакциями. Конечно 3/4 кода отдавать на служебные нужды это с одной стороны неприятно, с другой приемлемо. Но эти три четверти приходится повторять постоянно. Каждый метод, обращающийся к JPA, будет открывать persistence context, начинать транзакцию и завершать её. Сплошное самоповторение и скукота.

К счастью, это скукоту можно отдать на откуп , который реализует прекрасный интерфейс по управлению транзакциями. поддерживает глобальные транзакции, в которых участвует несколько участников и локальные транзакции, в которых участвует только один участник.  Последний случай более распространён и именно его мы и рассмотрим.

Подготовка

Нам понадобится пустой maven проект с Spring, Spring ORM, , Hibernate в качестве реализации JPA и библиотеками тестирования:

Настройка  JPA

Конфигурация JPA располагается в файле META-INF/persistence.xml В моём примере я использую Hibernate в качестве реализации JPA и встраиваемую базу H2 в качестве базы данных.

Разумеется, в настройках Hibernate как JPA реализации можно использовать любые базы данных и пулы соединений.

Настройка Spring и управления транзакциями

Для разнообразия настроим контекст Spring используя Java  конфигурацию. Это поможет продемонстрировать, как устроено управление транзакциями в Spring. Spring определяет интерфейс PlatformTransactionManager, для которого существуют разные реализации для использования с разными ресурсами.  Например, для JPA  используется JpaTransactionManager, который конфигурируется EntityManagerFactory:

Аннотация @EnableTransactionManagenent включает поддержку декларативного управления транзакциями, а создание JpaTransactionManager  подкладывает конкретную реализацию под управление транзакциями.

Аналогично можно создать менеджер транзакций для JDBC или Hibernate. Для JDBC следует использовать DataSourceTransactionManager, для Hibernate — HibernateTransactionManager.

 Уровень DAO

Схема данных абсолютно идентичная схеме, используемой в примере Spring ORM и JPA. А вот реализация уровня данных заметно отличается:

В первую очередь в класс внедряется непосредственно EntityManager, а не его фабрика. Во вторых, все методы получили аннотацию @Transactional. В третьих, из методов пропало явное управление транзакциями и создание EntityManager, все эти проблемы взял на себя Spring.

Аннотация @Transactional говорит Spring, что перед вызовом метода надо породить новый (не всегда новый, но об этом в следующей статье) persistence context (или запросить новое JDBC соединение) и начать в нём транзакцию. А после того как метод завершится, транзакцию необходимо подтвердить.

Кстати, если аннотацию @Transactional пропустить, то вызов такого метода вернёт ошибку:

Кроме того, у использования @Transactional есть неочевидное ограничение. Так как управление транзакциями реализовано путём создания прокси объектов времени исполнения, то при вызове метода с @Transactional напрямую, а не через прокси, то вызов провалится с такой же ошибкой. Если говорить проще, то вызывая метод у Spring bean вы в безопасности, во всех остальных случаях — нет:

@Transactional

Аннотация @Transactional поддерживает несколько параметров, которые задают поведение транзакции:

  • value и transactionManager — указывают, какой именно экземпляр PlatformTransactionManager использовать, если их несколько.
  • readOnly — указывает, что транзакция только читает данные, но не изменяет их. Это может быть использовано для оптимизации запросов или блокировок на уровне базы.
  • timeout — задаёт максимальную длительность операции на стороне базы данных и если эта длительность будет превышена, метод прервётся и транзакция откатится
  • rollbackFor/ rollbackForClassName — задают список классов исключений, которые вызовут откат транзакции, если метод их выбросит. В коде выше метод updateGreet() именно так откатывает транзакцию. По умолчанию, каждое исключение, имеющее в предках RuntimeError, вызывает откат транзакции.
  • noRollbackFor/ noRollbackForClassName — задают список классов исключений, которые не вызовут откат транзакции, если метод их выбросит.
  • propagation и isolation — управляют распространением транзакции и её уровнем изоляции. Я опишу эти параметры в отдельной статье.

В кратце метод с @Transactional можно рассматривать так: при входе в метод автоматически создаётся транзакция и открывается соединение с базой данных или создаётся persistence context. При выходе из функции транзакция автоматически подтверждается. Если функция кидает RuntimeError или его наследника (или настроенное исключение), транзакция автоматически откатывается. В любом случае, после выхода из метода соединение с базой закрывается, а persistence context разрушается.

Использование в приложении

Напишем сервис, который будет использовать транзакционное DAO и как нибудь его поиспользуем:

Тесты на DAO и на сервисе позволяют быть уверенными, что всё должно работать, но гораздо интереснее проверить в дикой природе:

Обратите внимание, что транзакция по смене данных откатилась и фактического изменения не произошло, как мы и ожидали.

Код примера доступен на github