Транзакция — это очень важное понятие в мире баз данных (и не только). Каждый, кто работал с базами данных, слышал про ACID и транзакции. Если кто не работал и не знает что это такое, то всё просто: транзакция это набор операций, которые могут быть либо целиком и успешно выполнены, либо полностью не выполнены.
Управление транзакциями поддерживается и в JDBC, и в JPA, и в Hibernate. И везде управление реализовано примерно одинаково:
- Открываем транзакцию
- Проводим какие-то действия над данными
- Подтверждаем, либо откатываем транзакцию
Например, код сохранения объекта в JPA может выглядеть так:
EntityManager em = emf.createEntityManager(); em.getTransaction().begin(); em.persist(g); em.getTransaction().commit();
На четыре строки кода только одна решает проблему разработчика, а все остальные нужны только для управления persistence context и транзакциями. Конечно 3/4 кода отдавать на служебные нужды это с одной стороны неприятно, с другой приемлемо. Но эти три четверти приходится повторять постоянно. Каждый метод, обращающийся к JPA, будет открывать persistence context, начинать транзакцию и завершать её. Сплошное самоповторение и скукота.
К счастью, это скукоту можно отдать на откуп Spring, который реализует прекрасный интерфейс по управлению транзакциями. Spring поддерживает глобальные транзакции, в которых участвует несколько участников и локальные транзакции, в которых участвует только один участник. Последний случай более распространён и именно его мы и рассмотрим.
Подготовка
Нам понадобится пустой maven проект с Spring, Spring ORM, H2, Hibernate в качестве реализации JPA и библиотеками тестирования:
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <javaee.version>7.0</javaee.version> <lombok.version>1.16.12</lombok.version> <org.springframework.version>4.3.4.RELEASE</org.springframework.version> <hibernate.version>5.2.5.Final</hibernate.version> <h2.version>1.4.190</h2.version> <junit.version>4.12</junit.version> <hamcrest.version>1.3</hamcrest.version> <easymock.version>3.3.1</easymock.version> </properties> <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-api</artifactId> <version>7.0</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${org.springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${org.springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${org.springframework.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>${org.springframework.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>${h2.version}</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.hamcrest</groupId> <artifactId>hamcrest-library</artifactId> <version>${hamcrest.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.easymock</groupId> <artifactId>easymock</artifactId> <version>${easymock.version}</version> </dependency> </dependencies>
Настройка JPA
Конфигурация JPA располагается в файле META-INF/persistence.xml В моём примере я использую Hibernate в качестве реализации JPA и встраиваемую базу H2 в качестве базы данных.
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="ru.easyjava.spring.data.jpa"> <properties> <property name="hibernate.hbm2ddl.auto" value="update"/> <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/> <property name="hibernate.connection.url" value="jdbc:h2:mem:test"/> </properties> </persistence-unit> </persistence>
Разумеется, в настройках Hibernate как JPA реализации можно использовать любые базы данных и пулы соединений.
Настройка Spring и управления транзакциями
Для разнообразия настроим контекст Spring используя Java конфигурацию. Это поможет продемонстрировать, как устроено управление транзакциями в Spring. Spring определяет интерфейс PlatformTransactionManager, для которого существуют разные реализации для использования с разными ресурсами. Например, для JPA используется JpaTransactionManager, который конфигурируется EntityManagerFactory:
@Configuration @EnableTransactionManagement public class DaoConfiguration { @Bean public LocalEntityManagerFactoryBean emf() { LocalEntityManagerFactoryBean result = new LocalEntityManagerFactoryBean(); result.setPersistenceUnitName("ru.easyjava.spring.data.transactions"); return result; } @Bean public PlatformTransactionManager transactionManager() { JpaTransactionManager result = new JpaTransactionManager(); result.setEntityManagerFactory(emf().getObject()); return result; } }
Аннотация @EnableTransactionManagenent включает поддержку декларативного управления транзакциями, а создание JpaTransactionManager подкладывает конкретную реализацию под управление транзакциями.
Аналогично можно создать менеджер транзакций для JDBC или Hibernate. Для JDBC следует использовать DataSourceTransactionManager, для Hibernate — HibernateTransactionManager.
Уровень DAO
Схема данных абсолютно идентичная схеме, используемой в примере Spring ORM и JPA. А вот реализация уровня данных заметно отличается:
public interface GreeterDao { void addGreet(Greeter g); void updateGreet(Greeter g, String newTarget); List<Greeter> getGreetings(); }
@Repository public class GreeterDaoImpl implements GreeterDao { @PersistenceContext private EntityManager em; @Override @Transactional public final void addGreet(final Greeter g) { em.persist(g); } @Override @Transactional(rollbackFor = NotImplementedException.class) public final void updateGreet(final Greeter g, final String newTarget) { Greeter greet = em.merge(g); greet.setTarget(newTarget); throw new NotImplementedException(); } @Override @Transactional(readOnly = true) public final List<Greeter> getGreetings() { return em.createQuery("from Greeter", Greeter.class) .getResultList(); } }
@ContextConfiguration(classes = ru.easyjava.spring.data.declarative.ContextConfiguration.class) @RunWith(SpringJUnit4ClassRunner.class) public class GreeterDaoImplIT { @Inject private GreeterDao testedObject; @DirtiesContext @Test public void testRetrieve() { Greeter expected = new Greeter(); expected.setGreeting("TEST"); expected.setTarget("TEST"); testedObject.addGreet(expected); List<Greeter> actual = testedObject.getGreetings(); Iterator<Greeter> it = actual.iterator(); Greeter actualGreet =it.next(); assertThat(actualGreet.getGreeting(), is("TEST")); assertThat(actualGreet.getTarget(), is("TEST")); } }
В первую очередь в класс внедряется непосредственно EntityManager, а не его фабрика. Во вторых, все методы получили аннотацию @Transactional. В третьих, из методов пропало явное управление транзакциями и создание EntityManager, все эти проблемы взял на себя Spring.
Аннотация @Transactional говорит Spring, что перед вызовом метода надо породить новый (не всегда новый, но об этом в следующей статье) persistence context (или запросить новое JDBC соединение) и начать в нём транзакцию. А после того как метод завершится, транзакцию необходимо подтвердить.
Кстати, если аннотацию @Transactional пропустить, то вызов такого метода вернёт ошибку:
javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread
Кроме того, у использования @Transactional есть неочевидное ограничение. Так как управление транзакциями реализовано путём создания прокси объектов времени исполнения, то при вызове метода с @Transactional напрямую, а не через прокси, то вызов провалится с такой же ошибкой. Если говорить проще, то вызывая метод у Spring bean вы в безопасности, во всех остальных случаях — нет:
class SomeDao { @PersistenceContext EntityManager em; @Transactional public void query() { em.persist(); } public void anotherQuery() { this.query(); //Вызов мимо прокси } } @Service class SomeService { @Inject SomeDao dao; public SomeMethod() { dao.query; //Это работает } public OtherMethod() { dao.anotherQuery(); //А это провалится } }
@Transactional
Аннотация @Transactional поддерживает несколько параметров, которые задают поведение транзакции:
- value и transactionManager — указывают, какой именно экземпляр PlatformTransactionManager использовать, если их несколько.
- readOnly — указывает, что транзакция только читает данные, но не изменяет их. Это может быть использовано для оптимизации запросов или блокировок на уровне базы.
- timeout — задаёт максимальную длительность операции на стороне базы данных и если эта длительность будет превышена, метод прервётся и транзакция откатится
- rollbackFor/ rollbackForClassName — задают список классов исключений, которые вызовут откат транзакции, если метод их выбросит. В коде выше метод updateGreet() именно так откатывает транзакцию. По умолчанию, каждое исключение, имеющее в предках RuntimeError, вызывает откат транзакции.
- noRollbackFor/ noRollbackForClassName — задают список классов исключений, которые не вызовут откат транзакции, если метод их выбросит.
- propagation и isolation — управляют распространением транзакции и её уровнем изоляции. Я опишу эти параметры в отдельной статье.
В кратце метод с @Transactional можно рассматривать так: при входе в метод автоматически создаётся транзакция и открывается соединение с базой данных или создаётся persistence context. При выходе из функции транзакция автоматически подтверждается. Если функция кидает RuntimeError или его наследника (или настроенное исключение), транзакция автоматически откатывается. В любом случае, после выхода из метода соединение с базой закрывается, а persistence context разрушается.
Использование в приложении
Напишем сервис, который будет использовать транзакционное DAO и как нибудь его поиспользуем:
public interface GreeterService { String greet(); }
@Service public class GreeterServiceImpl implements GreeterService { @Inject private GreeterDao dao; @Override public final String greet() { List<Greeter> greets = dao.getGreetings(); Iterator<Greeter> it = greets.iterator(); if (!it.hasNext()) { return "No greets"; } Greeter greeter = it.next(); return greeter.getGreeting() + ", " + greeter.getTarget(); } }
public class GreeterServiceImplTest extends EasyMockSupport { @Rule public EasyMockRule em = new EasyMockRule(this); @Mock private GreeterDao dao; @TestSubject private GreeterServiceImpl testedObject = new GreeterServiceImpl(); @Test public void testNoGreets() { expect(dao.getGreetings()).andReturn(Collections.EMPTY_LIST); replayAll(); assertThat(testedObject.greet(), is("No greets")); } @Test public void testGreets() { Greeter expected = new Greeter(); expected.setGreeting("TEST"); expected.setTarget("TEST"); expect(dao.getGreetings()).andReturn(Collections.singletonList(expected)); replayAll(); assertThat(testedObject.greet(), is("TEST, TEST")); } }
Тесты на DAO и на сервисе позволяют быть уверенными, что всё должно работать, но гораздо интереснее проверить в дикой природе:
public static void main(final String[] args) { ApplicationContext context = new AnnotationConfigApplicationContext(ContextConfiguration.class); GreeterService greeterService = context.getBean(GreeterService.class); GreeterDao dao = context.getBean(GreeterDao.class); Greeter greeter = new Greeter(); greeter.setGreeting("Hello"); greeter.setTarget("World"); dao.addGreet(greeter); System.out.println(greeterService.greet()); try { dao.updateGreet(greeter, "Fail"); } catch (NotImplementedException e) { // Do nothing } System.out.println(greeterService.greet()); System.exit(0); }
Dec 26, 2016 2:23:12 PM org.springframework.orm.jpa.LocalEntityManagerFactoryBean buildNativeEntityManagerFactory INFO: Initialized JPA EntityManagerFactory for persistence unit 'ru.easyjava.spring.data.transactions' Dec 26, 2016 2:23:12 PM org.hibernate.hql.internal.QueryTranslatorFactoryInitiator initiateService INFO: HHH000397: Using ASTQueryTranslatorFactory Hello, World Hello, World
Обратите внимание, что транзакция по смене данных откатилась и фактического изменения не произошло, как мы и ожидали.