Транзакция — это очень важное понятие в мире баз данных (и не только). Каждый, кто работал с базами данных, слышал про ACID и транзакции. Если кто не работал и не знает что это такое, то всё просто: транзакция это набор операций, которые могут быть либо целиком и успешно выполнены, либо полностью не выполнены.
Управление транзакциями поддерживается и в JDBC, и в JPA, и в Hibernate. И везде управление реализовано примерно одинаково:
- Открываем транзакцию
- Проводим какие-то действия над данными
- Подтверждаем, либо откатываем транзакцию
Например, код сохранения объекта в JPA может выглядеть так:
1
2
3
4
|
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 и библиотеками тестирования:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
|
<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 в качестве базы данных.
1
2
3
4
5
6
7
8
9
10
11
12
|
<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:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
@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. А вот реализация уровня данных заметно отличается:
1
2
3
4
5
|
public interface GreeterDao {
void addGreet(Greeter g);
void updateGreet(Greeter g, String newTarget);
List<Greeter> getGreetings();
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
@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();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
@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 пропустить, то вызов такого метода вернёт ошибку:
1
|
javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread
|
Кроме того, у использования @Transactional есть неочевидное ограничение. Так как управление транзакциями реализовано путём создания прокси объектов времени исполнения, то при вызове метода с @Transactional напрямую, а не через прокси, то вызов провалится с такой же ошибкой. Если говорить проще, то вызывая метод у Spring bean вы в безопасности, во всех остальных случаях — нет:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
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 и как нибудь его поиспользуем:
1
2
3
|
public interface GreeterService {
String greet();
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@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();
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
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 и на сервисе позволяют быть уверенными, что всё должно работать, но гораздо интереснее проверить в дикой природе:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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);
}
|
1
2
3
4
5
6
|
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
|
Обратите внимание, что транзакция по смене данных откатилась и фактического изменения не произошло, как мы и ожидали.
Код примера доступен на github