Внутренние транзакции не работают
context.xml:
<context:component-scan base-package="BP"/>
<tx:annotation-driven transaction-manager="txManager"/><!-- a PlatformTransactionManager is still required -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- (this dependency is defined somewhere else) -->
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/springjdbc?serverTimezone=UTC"/>
<property name="username" value="root"/>
<property name="password" value="1234"/>
</bean>
Класс, в котором происходит действие(Находится в пакете BP):
@Configuration
public class SomeNumbersMySQLDao{
public JdbcTemplate jdbcTemplate;
@Autowired
public SomeNumbersMySQLDao(DataSource dataSource){
jdbcTemplate = new JdbcTemplate(dataSource);
}
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("context.xml");
SomeNumbersMySQLDao dao = context.getBean("someNumbersMySQLDao", SomeNumbersMySQLDao.class);
// код здесь будет изменяться. Назовем ее точкой "main"
}
// здесь методы, которые покажу далее.
public void cleanTable() {
jdbcTemplate.execute("DELETE FROM someNumbers");
}
public void printTable() {
List<Integer> numbersGot = jdbcTemplate.queryForList("SELECT * FROM someNumbers", Integer.class);
System.out.println("Numbers got from table: ");
numbersGot.forEach(System.out::println);
}
}
Транзакции на одиночных(не вложенных) методах работают.
Приведу пример.
Есть метод:
@Transactional(propagation=Propagation.REQUIRED)
public void outer(int number) {
System.out.println(TransactionSynchronizationManager.isActualTransactionActive());
jdbcTemplate.update("INSERT INTO someNumbers (number) VALUES (?)", new Object[] {number});
throw new RuntimeException();
}
В точке "main" пишем следующий код:
dao.cleanTable();
dao.outer(123);
Выполняем, убираем этот код, пишем следующий код и вновь выполняем:
dao.printTable();
Получим сначала вывод:
true
а затем:
Numbers got from table:
Что понятно, так как транзакция откатилась. Если мы напишем вместо Propagation.REQUIRED Propagation.NEVER, то получим сначала вывод:
false
а затем:
Numbers got from table:
123
Что логично - транзакции нет, она и не откатилась.
Более того, если мы напишем вместо Propagation.REQUIRED Propagation.MANDATORY, то получим однозначный(что одиночные транзакции работают) вывод:
Exception in thread "main" org.springframework.transaction.IllegalTransactionStateException
Хорошо, теперь попробуем одним методом с Propagation.REQUIRED вызвать метод с Propagation.NEVER. По идее должны получить ошибку, но этого не происходит.
@Transactional(propagation=Propagation.REQUIRED)
public void outer(int number) {
System.out.println(TransactionSynchronizationManager.isActualTransactionActive());
jdbcTemplate.update("INSERT INTO someNumbers (number) VALUES (?)", new Object[] {number});
withNever(number);
}
@Transactional(propagation=Propagation.NEVER)
public void withNever(int numberToInsertInInnerMethod) {
System.out.println(TransactionSynchronizationManager.isActualTransactionActive());
jdbcTemplate.update("INSERT INTO someNumbers (number) VALUES (?)", new Object[] {numberToInsertInInnerMethod});
throw new RuntimeException();
}
Вывод:
true
true
Exception in thread "main" java.lang.RuntimeException
а затем:
Numbers got from table:
Должно было лечь уже на вызове метода Propagation.NEVER. И изменения откатились.
Та же картина и с Propagation.REQUIRES_NEW, и с Propagation.REQUIRED, и тем более с Propagation.MANDATORY.
Однако несколько другие результаты, если поставить на метод outer Propagation.NEVER. Тогда(пробовал пока только на Propagation.REQUIRED, но уверен, что то же будет и с остальными) будет следующий вывод:
false
false
Exception in thread "main" java.lang.RuntimeException
И потом:
Numbers got from table:
123
123
То есть мои внутренние транзакции по-сути работают как транзакции Propagation.SUPPORTS.
Ответы (1 шт):
Механизм управления транзакциями по умолчанию использует Spring AOP. И в данном случае вы столкнулись с ограничением Spring AOP, а именно, что вложенные вызовы используя this этим механизмом не поддерживаются.
Дело в том, что Spring AOP не расширяет класс, а создает для класса обертку (proxy). И в места, где внедряется этот бин или при вызове getBean используется именно обертка. Эта обертка и занимается управлением транзакцией, т.е. стартом, фиксированием результата (commit) при успешном выполнении или отменой при ошибке.
При вызове же метода через this обертка не используется - это прямой вызов, и соответственно, механизм управления транзакциями не задействуется.
Теперь, когда разобрались с причиной, рассмотрим какие есть варианты это исправить.
Использование AspectJ вместо Spring AOP
Во-первых, нужно изменить конфигурацию, чтобы механизм управления транзакциями использовал именно AspectJ:
<tx:annotation-driven mode="aspectj"/>
Далее, нужно задействовать связывание (weaving) для AspectJ. Есть несколько способов это делать, я здесь описывать не буду. Подробности как это делать описаны в документации тут и тут.
Изменение структуры кода
Можно изменить структуру кода, чтобы не было вызовов к транзакционным методам внутри одного класса. В данном случае речь идет о том, чтоб перенести withNever в отдельный класс.
Использование самовнедрения (self-injection)
Можно внедрить ссылку на бин в самого себя и использовать ее вместо this:
public class SomeNumbersMySQLDao{
public JdbcTemplate jdbcTemplate;
@Autowired
private SomeNumbersMySQLDao someNumbersMySQLDao;
@Transactional(propagation=Propagation.REQUIRED)
public void outer(int number) {
...
someNumbersMySQLDao.withNever(number);
}
@Transactional(propagation=Propagation.NEVER)
public void withNever(int numberToInsertInInnerMethod) {
...
}
}
Использовать класс посредник
@Service
public class TransactionHandler {
@Transactional(propagation = Propagation.REQUIRED)
public <T> T runInTransaction(Supplier<T> supplier) {
return supplier.get();
}
@Transactional(propagation = Propagation.NEVER)
public <T> T runOutOfTransaction(Supplier<T> supplier) {
return supplier.get();
}
}
public class SomeNumbersMySQLDao{
@Autowired
private TransactionHandler transactionHandler;
@Transactional(propagation=Propagation.REQUIRED)
public void outer(int number) {
...
transactionHandler.runOutOfTransaction(() -> withNever(number));
}
@Transactional(propagation=Propagation.NEVER)
public void withNever(int numberToInsertInInnerMethod) {
...
}
}
Важно, что тут настройки вложенной транзакции определяются не @Transactional на withNever, а настройками @Transactoinal метода runOutOfTransaction.
Использовать ручное управление транзакциями
public class SomeNumbersMySQLDao{
@Autowired
private TransactionTemplate neverPropagateTransactionTemplate;
@Transactional(propagation=Propagation.REQUIRED)
public void outer(int number) {
...
neverPropagateTransactionTemplate.execute(() -> withNever(number));
}
@Transactional(propagation=Propagation.NEVER)
public void withNever(int numberToInsertInInnerMethod) {
...
}
}
Нужно понимать, что в этом случае параметры указанные в атрибутах Transactional, такие как propagation, isolation и все другие не учитываются, а берутся те, которые заданы в конфигурации neverPropagateTransactionTemplate:
<bean id="neverPropagateTransactionTemplate"
class="org.springframework.transaction.support.TransactionTemplate">
<property name="propagationBehaviourName" value="PROPAGATION_NEVER"/>
</bean>