如何在@Transactional方法中手动强制提交?

83

我正在使用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()的方法。


你可以研究一下是否有办法让生成的线程参与事务,这样它们就能看到未提交的结果。 - Jim Garrison
如果一个方法被@Transactional注解标记,从方法中返回将提交事务。那么为什么不直接从方法中返回呢? - Raedwald
从概念上讲,单元测试可能不应该是事务性的,在Spring的模型中也没有实际意义。您应该查看使用Spring TestContext的集成测试,它具有帮助处理事务的工具:http://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/testing.html#testing-tx - Matt Whipple
@JimGarrison 实际上,我的单元测试的整个重点是测试并行事务,并验证事务中没有并发问题。 - Eric B.
@Raedwald 如果我从方法中返回,我该如何继续我的测试?我需要在线程生成之前提交,因为线程使用在它们生成之前创建的数据。 - Eric B.
顺便提一下,如果您正在使用存储库,则可以在JPARepository上使用flush()方法。请参见http://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/JpaRepository.html#flush-- - Josh
3个回答

105

我在测试只有在提交时才会调用的Hibernate事件监听器时遇到了类似的用例。

解决方案是将要持久化的代码包装到另一个使用REQUIRES_NEW注释的方法中。(在另一个类中)这样就生成了一个新的事务,并在该方法返回后发出刷新/提交。

Tx prop REQUIRES_NEW

请注意,这可能会影响所有其他测试!因此,请相应地编写它们或确保您可以在测试运行后清理它们。


6
为什么要说“另一个班级”? - Basemasta
35
由于Spring会使用代理来实现这个特性(还有很多其他特性),因此您需要使用另一个类,以便Spring能够触发事务。一旦您进入了一个类,就不会再次经过代理。 - Martin Frey
这可以是一个内部类吗? - Gleeb
1
可能可以,只要它是一个Spring Bean。我没有这样测试过。你可以试试 :) - Martin Frey
如果您有一个嵌套事务,我认为您的意图是不想挂起外部事务。例如,在流保持打开的情况下。 - html_programmer
显示剩余3条评论

28
为什么不使用Spring的TransactionTemplate来以编程方式控制事务?您还可以重构代码,使每个“事务块”都有自己的@Transactional方法,但考虑到这是一个测试,我建议采用编程方式控制事务。
另请注意,您在可运行对象上的@Transactional注释将无效(除非您正在使用aspectj),因为可运行对象不受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;
        }
    });
}

}


谢谢您的建议。我曾经考虑过这个方法,但是看起来对于一个简单的问题来说有点繁琐了。我原本以为会有更简单的方法。但是将其拆分成一个使用Propagation.REQUIRES_NEW的单独方法就解决了这个问题(请参见@MartinFrey的答案)。 - Eric B.
1
对于我的情况,只有使用transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); 才有效。在此查看代码链接 - Grigory Kislin

7

我知道由于使用了丑陋的匿名内部类,TransactionTemplate的使用看起来并不好看,但是当我们出于某种原因想要在测试方法中进行事务处理时,它是最灵活的选项。

在一些情况下(这取决于应用程序类型),在Spring测试中使用事务的最佳方式是将测试方法上的@Transactional关闭。为什么?因为@Transactional可能会导致许多错误的测试结果。您可以查看这篇示例文章以获取详细信息。在这种情况下,当我们需要控制事务边界时,TransactionTemplate可以完美地发挥作用。


嗨,我想知道这是否正确:在创建保存对象的语句的集成测试时,建议刷新实体管理器,以避免任何虚假负面结果,即避免测试运行良好但在生产环境中运行失败的情况。实际上,测试可能会正常运行,仅因为第一级缓存未被刷新且没有写入命中数据库。为避免这种虚假负面集成测试,请在测试主体中使用显式刷新。 - Stephane
2
@StephaneEybert 在我看来,只是清空EntityManager更像是一种hack :-) 但这取决于您的需求和测试类型。对于真正的集成测试(应该与生产环境完全一致),答案是:绝对不要在测试周围使用@Transactional。但是也存在缺点:您必须在每次此类测试之前设置数据库为已知状态。 - G. Demecki

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接