web-dev-qa-db-fra.com

Comment forcer manuellement un commit dans une méthode @Transactional?

J'utilise Spring/Spring-data-JPA et je me trouve dans l'obligation de forcer manuellement un commit dans un test unitaire. Mon cas d'utilisation est que je suis en train de faire un test multi-thread dans lequel je dois utiliser des données qui sont persistées avant que les threads ne soient générés.

Malheureusement, étant donné que le test s'exécute dans une transaction @Transactional, Même un flush ne le rend pas accessible aux threads générés.

   @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());
    }

J'ai essayé d'utiliser le gestionnaire d'entités pour, mais je reçois un message d'erreur lorsque je le fais:

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)

Existe-t-il un moyen de valider la transaction et de la poursuivre? J'ai été incapable de trouver une méthode qui me permette d'appeler une commit().

48
Eric B.

J'ai eu un cas d'utilisation similaire lors du test des écouteurs d'événements Hibernate qui ne sont appelés que sur commit.

La solution consistait à envelopper le code pour qu'il soit persistant dans une autre méthode annotée avec REQUIRES_NEW. (Dans une autre classe) Ainsi, une nouvelle transaction est générée et un flush/commit est émis une fois la méthode retournée.

Tx prop REQUIRES_NEW

Gardez à l'esprit que cela pourrait influencer tous les autres tests! Alors écrivez-les en conséquence ou vous devez vous assurer que vous pouvez nettoyer après le test.

53
Martin Frey

Pourquoi n'utilisez-vous pas les commandes de printemps TransactionTemplate pour contrôler par programme les transactions? Vous pouvez également restructurer votre code pour que chaque "bloc de transaction" ait son propre @Transactional _ méthode, mais étant donné qu’il s’agit d’un test, j’opterais pour un contrôle programmatique de vos transactions.

Notez également que le @Transactional L'annotation sur votre runnable ne fonctionnera pas (sauf si vous utilisez aspectj) car les runnables ne sont pas gérées par spring!

@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;
        }
    });
}

}

23
Pieter

Je sais qu’en raison de cette utilisation anormale et anonyme de la classe interne de TransactionTemplate ne semble pas agréable, mais lorsque, pour une raison quelconque, nous voulons une méthode de test transactionnelle à mon humble avis, c’est la option la plus flexible .

Dans certains cas (cela dépend du type d'application), la meilleure façon d'utiliser les transactions dans les tests Spring consiste à désactiver @Transactional Sur les méthodes de test. Pourquoi? Parce que @Transactional Peut conduire à de nombreux tests de faux positifs. Vous pouvez regarder ceci exemple d'article pour en savoir plus. Dans de tels cas, TransactionTemplate peut être parfait pour contrôler les limites de transaction lorsque nous voulons ce contrôle.

4
G. Demecki