Сталкивались вы когда-нибудь с вложенными транзакциями?
В двух словах: эта такая транзакция которая создается внутри родительской транзакции. И может быть обработана независимо от основной.
Но проще это объяснить на примере.
Например. Например вам нужно сделать сервис, который будет регулярно опрашивать базу данных и проводить обработку транзакций. Каждый такой вызов транзакции должен сопровождаться обращением в интернет или какую-другую нестабильную среду и в результате чего может образоваться ошибка (или что более правильно исключение).
Вот как-то так.
Что бы обеспечить стабильность, вы должны обрабоать эту ошибку и в дальнейшем привизать эту ошибку к транзакции.
То есть простым языком получаете список необработанных транзакций. Вызываете функцию обработки транзакции и ошибку записываете в другое поле той же транзакции.
Как все это реализовать на Hibernate \ Spring?
Ну в документации сказано что есть такое средство. Называется nested transaction. Для его активации у метода, создающего вложенную транзакцию необходимо использовать атрибут PROPAGATION_NESTED. А еще прописать в tarnsactionmanager проперти nestedTransactionAllowed со значением true.
Вот как выглядит эта теория на практике:
dao.xml
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
<property name="nestedTransactionAllowed" value="true" />
</bean>
IncomeProcessor.java
@Service
@Transactional
public class IncomeProcessor {
[...]
@Scheduled(fixedRate = 60 * 1000)
public void processNormal() {
List<Transaction> list = some.list();
for(Transaction t : list) {
try {
process(t);
} catch(Exception e){
t.setError(e);
dao.update(t);
}
}
@Transactional(propagation = Propagation.NESTED)
public void process(Transaction t) {
doSomeExternalJob(t);
}
}
В примере мы получем список, пробегаемся по всем эелемнтам и все ошибки (исключения) записываем в объект transaction в базу данных. В териории все исключения возникшие в вызове process(), обернуты обработчиком откатывают все изменения в базе данных сделаные методом doSomeExternalJob(). Таким образом мы не теряем данные и не захламляем базу новыми объектами созданными до возникновения исключения.
Но на практике такой подход не работает. Я не стал разбераться ошибка ли это хибернейта или jdbc драйвера. Я стал разбераться в том как эти вложенные транзакции реализованы и нашел сразу две довольно критичные проблемы в приведенной выше схеме. Так что стандартный механизм спринга лучше в таком варианте не использовать, если не хотите потерять данные (или пропустить обработку).
Прежде всего хочу сказать как это должно быть реализовано в jdbc драйвере.
При обработке вложенных транзакций драйвер, в точке входа в функцию создает так называемые Savepoint. И при выходе, в случае возникновения ошибки вызвает функцию у соединения rollback() с передачей ей нужной точки сохранения. Таким образом при входе в фукнцию process должна была бы вызваться connection.setSavepoint() и при выходе, для отката части транзакции connection.rollback(savePoint).
Почему это не работает, непонятно. Можно покапаться в исходниках sping / hibernate благо они доступны и найти что нибудь глупое навроде не правильной версии jdbc или пропуск создания какогонибудь хитрого bean с указанной логикой. Я решил не искать проблему и не создавать баг репорта по причине что я нашел вторую проблему. Так же связанную с привиденным выше алгоритмом работы. И эта ситуация на корню блокирует любые попытки программно (с помощью атрибутов) сделать элегантное решение этих nested transactions.
В чем эта вторая проблема заключается и почему исправление вложных транзакций так сложно?
Я провел простой эксперимент. Провел симуляцию правильной логики сохранения \ откатов savepoints и обнаржил что не все объекты всстанавливают свое исходное состояние. Несмотря на то что откат проходит успешно некоторые объекты все равно остаются в своем прежнем состоянии как до отката. Отмененная транзакция до указанного savepoint ни как не затрагивает объектов уже скаченных с сервера, закешированных и измененных на памяти.
Что это означает. Если вы создаете savepoint для текущей транзакции и передаете в фукнцию обработки объект транзакция то не смотря на то что вызываете rollback нет такого маханизма в спринге что бы отслеживать объекты измененные на памяти и запрашивать автоматически их состояния с сервера.
Что привело меня к мысли что исправление вложенных транзакций все равно не повзолит реализовать указанную лоигку только на стороне спринг \ хибернейт библиотек. Так как перечитывать состояние всех объектов после отката одной транзакции не имеет практического смысла и к тому же приведет к потере данных. А откатывать отдельный параметр было бы так же не допустимо.
В результате этих рассуждений я решил реализовать все это руками, с исправлением указанных проблем. Привожу работающий код:
NestedTransaction.java
package com.payment.db.services;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Savepoint;
import java.util.List;
import java.util.Stack;
import org.hibernate.SessionFactory;
import org.hibernate.jdbc.Work;
public abstract class NestedTransaction<T> {
public void list(SessionFactory sessionFactory, List<T> list) {
final Stack<Savepoint> s = new Stack<Savepoint>();
for (T e : list) {
try {
sessionFactory.getCurrentSession().doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
s.push(connection.setSavepoint());
}
});
process(e);
} catch (Exception ee) {
sessionFactory.getCurrentSession().doWork(new Work() {
@Override
public void execute(Connection connection) throws SQLException {
connection.rollback(s.pop());
}
});
sessionFactory.getCurrentSession().refresh(e);
exception(e, ee);
}
}
}
public abstract void process(T e);
public abstract void exception(T e, Exception ee);
}
IncomeProcessor.java
package com.payment.db.services;
@Service
@Transactional
public class IncomeProcessor extends NestedTransaction<ChargePool> {
[...]
@Scheduled(fixedRate = 60 * 1000)
public void processNormal() {
List<ChargePool> list = chargepool.listAvailable();
if (list == null || list.size() == 0) {
return;
}
list(sessionFactory, list);
}
public void process(ChargePool cp) {
Charge c = cp.getCharge();
if (!c.isLinked())
throw new RuntimeException("operation on non linked charge");
SomeProcessIncome(cp);
}
public void exception(ChargePool cp, Exception e) {
cp.setLastOperationRespond(ExceptionText.covert(e));
cp.setLastOperationDate(new Date());
chargepool.update(cp);
}
}
В приведенном примере создан базовый класс для обработки вложенных транзакций с использованием фукнций сохранения \ отката состояний (Savepoint) и так же исправлена проблема с откатом объектов на памяти вызовом фукнции sessionFactory.getCurrentSession().refresh(e)