Как вручную принудительно выполнить фиксацию в методе @Transactional?

Я использую Spring/Spring-data-JPA и обнаружил, что мне нужно вручную принудительно выполнить фиксацию в модульном тесте. Мой вариант использования заключается в том, что я выполняю многопоточный тест, в котором я должен использовать данные, которые сохраняются до создания потоков.

К сожалению, учитывая, что тест выполняется в транзакции @Transactional, даже flush не делает его доступным для порожденных потоков.

   @Transactional   
   public void testAddAttachment() throws Exception{
        final Contract c1 = contractDOD.getNewTransientContract(15);
        contractRepository.save(c1);

        // Need to commit the saveContract here, but don't know how!                
        em.getTransaction().commit();

        List<Thread> threads = new ArrayList<>();
        for( int i = 0; i < 5; i++){
            final int threadNumber = i; 
            Thread t =  new Thread( new Runnable() {
                @Override
                @Transactional
                public void run() {
                    try {
                        // do stuff here with c1

                        // sleep to ensure that the thread is not finished before another thread catches up
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            });
            threads.add(t);
            t.start();
        }

        // have to wait for all threads to complete
        for( Thread t : threads )
            t.join();

        // Need to validate test results.  Need to be within a transaction here
        Contract c2 = contractRepository.findOne(c1.getId());
    }

Я пытался использовать диспетчер сущностей, но получаю сообщение об ошибке, когда делаю:

org.springframework.dao.InvalidDataAccessApiUsageException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead; nested exception is java.lang.IllegalStateException: Not allowed to create transaction on shared EntityManager - use Spring transactions or EJB CMT instead
    at org.springframework.orm.jpa.EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(EntityManagerFactoryUtils.java:293)
    at org.springframework.orm.jpa.aspectj.JpaExceptionTranslatorAspect.ajc$afterThrowing$org_springframework_orm_jpa_aspectj_JpaExceptionTranslatorAspect$1$18a1ac9(JpaExceptionTranslatorAspect.aj:33)

Есть ли способ зафиксировать транзакцию и продолжить ее? Мне не удалось найти какой-либо метод, позволяющий мне вызывать commit().


person Eric B.    schedule 21.06.2014    source источник
comment
Вы можете исследовать, есть ли способ заставить порожденные потоки участвовать в транзакции, чтобы они видели незафиксированные результаты.   -  person Jim Garrison    schedule 21.06.2014
comment
Если метод @Transactional, возврат из метода фиксирует транзакцию. Так почему бы просто не вернуться из метода?   -  person Raedwald    schedule 21.06.2014
comment
Концептуально модульные тесты, вероятно, не должны быть транзакционными, а с моделью Spring это также не имеет практического смысла. Вам следует изучить интеграционные тесты с использованием Spring TestContext, в котором есть инструменты для помощи с транзакциями: docs.spring.io/spring/docs/3.2.x/spring-framework-reference/   -  person Matt Whipple    schedule 21.06.2014
comment
@JimGarrison На самом деле весь смысл моего модульного теста состоит в том, чтобы протестировать параллельные транзакции и убедиться, что в транзакциях нет проблем с параллелизмом.   -  person Eric B.    schedule 22.06.2014
comment
@Raedwald, если я вернусь из метода, как мне продолжить тест? Мне нужна фиксация до появления моих потоков, поскольку потоки используют данные, созданные до их создания.   -  person Eric B.    schedule 22.06.2014
comment
Кстати, если вы работаете с репозиториями, у вас есть метод flush() в вашем JPARepository. См. docs.spring.io/spring-data/jpa/docs/current/api/org/   -  person Josh    schedule 22.08.2016


Ответы (3)


У меня был аналогичный вариант использования во время тестирования прослушивателей событий гибернации, которые вызываются только при фиксации.

Решение состояло в том, чтобы обернуть код, чтобы он был постоянным, в другой метод, аннотированный REQUIRES_NEW. (В другом классе) Таким образом создается новая транзакция, и после возврата метода выполняется сброс/фиксация.

Tx prop REQUIRES_NEW

Имейте в виду, что это может повлиять на все остальные тесты! Поэтому напишите их соответствующим образом, иначе вам нужно убедиться, что вы можете очистить их после запуска теста.

person Martin Frey    schedule 21.06.2014
comment
Блестящий. Не думал об этом. Придется немного разбить мой метод тестирования, что меня не слишком радует, но он отлично работает. - person Eric B.; 22.06.2014
comment
@MartinFrey Почему в другом классе? - person Basemasta; 07.09.2014
comment
Поскольку Spring работает с прокси-серверами для достижения этой функции (и многих других), вам нужно использовать другой класс, чтобы Spring мог запускать транзакции. Как только вы окажетесь внутри класса, вы больше не будете использовать прокси. - person Martin Frey; 07.09.2014
comment
Может ли это быть внутренним классом? - person Gleeb; 11.05.2015
comment
Может быть, пока это весенняя фасоль. Такого не тестировал. Попробуй :) - person Martin Frey; 11.05.2015
comment
Не могли бы вы опубликовать фрагмент кода? Вы делаете то, что делаете, но в конце теста фиксируется только одна транзакция. - person PragmaticProgrammer; 29.06.2018
comment
Как REQUIRES_NEW собирается решить проблему? Как вы сказали, он просто приостанавливает родительскую транзакцию и не обязательно фиксирует ее (родительскую). Означает ли это, что родитель ждет возвращения дочернего элемента, и только тогда происходит фиксация родителя? Если это так, то это не 100% решение из-за того, как работает многопоточность. Это просто увеличивает шансы избежать состояния гонки - person Aladin; 30.07.2020
comment
Если у вас есть вложенная транзакция, я предполагаю, что дело в том, что вы не хотите приостанавливать внешнюю транзакцию. Например. в случае потоков, которые вы хотите оставить открытыми. - person html_programmer; 11.06.2021

Почему бы вам не использовать весенний TransactionTemplate программно управлять транзакциями? Вы также можете реструктурировать свой код, чтобы каждый «блок транзакций» имел свой собственный метод @Transactional, но, учитывая, что это тест, я бы выбрал программный контроль ваших транзакций.

Также обратите внимание, что аннотация @Transactional на вашем runnable не будет работать (если вы не используете аспект j), так как spring не управляет runnables!

@RunWith(SpringJUnit4ClassRunner.class)
//other spring-test annotations; as your database context is dirty due to the committed transaction you might want to consider using @DirtiesContext
public class TransactionTemplateTest {

@Autowired
PlatformTransactionManager platformTransactionManager;

TransactionTemplate transactionTemplate;

@Before
public void setUp() throws Exception {
    transactionTemplate = new TransactionTemplate(platformTransactionManager);
}

@Test //note that there is no @Transactional configured for the method
public void test() throws InterruptedException {

    final Contract c1 = transactionTemplate.execute(new TransactionCallback<Contract>() {
        @Override
        public Contract doInTransaction(TransactionStatus status) {
            Contract c = contractDOD.getNewTransientContract(15);
            contractRepository.save(c);
            return c;
        }
    });

    ExecutorService executorService = Executors.newFixedThreadPool(5);

    for (int i = 0; i < 5; ++i) {
        executorService.execute(new Runnable() {
            @Override  //note that there is no @Transactional configured for the method
            public void run() {
                transactionTemplate.execute(new TransactionCallback<Object>() {
                    @Override
                    public Object doInTransaction(TransactionStatus status) {
                        // do whatever you want to do with c1
                        return null;
                    }
                });
            }
        });
    }

    executorService.shutdown();
    executorService.awaitTermination(10, TimeUnit.SECONDS);

    transactionTemplate.execute(new TransactionCallback<Object>() {
        @Override
        public Object doInTransaction(TransactionStatus status) {
            // validate test results in transaction
            return null;
        }
    });
}

}

person Pieter    schedule 21.06.2014
comment
Спасибо за идею. Думал об этом, но выглядел как много работы / излишеств для простой проблемы. Предполагал, что должно быть что-то намного проще. Разбив его на отдельный метод с помощью Propagation.REQUIRES_NEW, мы выполнили именно это (см. ответ @MartinFrey). - person Eric B.; 22.06.2014
comment
В моем случае это работает только с transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); См. код здесь - person Grigory Kislin; 26.09.2017

Я знаю, что из-за этого уродливого анонимного внутреннего класса использование TransactionTemplate выглядит не очень хорошо, но когда по какой-то причине мы хотим иметь транзакционный метод тестирования, ИМХО, это наиболее гибкий вариант.

В некоторых случаях (это зависит от типа приложения) лучшим способом использования транзакций в тестах Spring является отключение @Transactional в методах тестирования. Почему? Потому что @Transactional может привести ко многим ложноположительным тестам. Вы можете просмотреть этот пример статьи, чтобы узнать подробности. . В таких случаях TransactionTemplate может идеально подойти для контроля границ транзакций, когда нам нужен этот контроль.

person G. Demecki    schedule 23.10.2014
comment
Привет, интересно, так ли это: при создании интеграционного теста для инструкции, сохраняющей объект, рекомендуется сбросить диспетчер сущностей, чтобы избежать ложных отрицательных результатов, то есть избежать нормального выполнения теста, но операция которого завершится ошибкой. при запуске в производство. Действительно, тест может пройти нормально просто потому, что кэш первого уровня не очищается и в базу данных не попадает запись. Чтобы избежать этого ложноотрицательного интеграционного теста, используйте явный сброс в теле теста. - person Stephane; 08.10.2015
comment
@StephaneEybert IMO, просто сброс EntityManager больше похож на взлом :-) Но это зависит от ваших потребностей и типа тестов, которые вы делаете. Для настоящих интеграционных тестов (которые должны вести себя точно так же, как в продакшене) ответ таков: определенно не используйте @Transactional вокруг теста. Но есть и недостаток: вы должны настроить базу данных в хорошо известное состояние перед каждым таким тестом. - person G. Demecki; 09.10.2015