如何在@Transactional 方法中手动强制提交? [复制]
Posted
技术标签:
【中文标题】如何在@Transactional 方法中手动强制提交? [复制]【英文标题】:How to manually force a commit in a @Transactional method? [duplicate] 【发布时间】:2014-08-11 20:29:33 【问题描述】:我正在使用 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()
。
【问题讨论】:
您可能会研究是否有办法让生成的线程参与事务,以便他们看到未提交的结果。 如果方法是@Transactional
,从方法返回提交事务。那么为什么不直接从方法中返回呢?
从概念上讲,单元测试不应该是事务性的,而且对于 Spring 的模型,它也没有实际意义。您应该查看使用 Spring TestContext 的集成测试,它具有帮助处理事务的工具:docs.spring.io/spring/docs/3.2.x/spring-framework-reference/…
@JimGarrison 实际上我的单元测试的重点是测试并行事务并验证事务中没有并发问题。
@Raedwald 如果我从该方法返回,我该如何继续我的测试?我需要在我的线程产生之前提交,因为线程使用在它们产生之前创建的数据。
【参考方案1】:
在测试仅在提交时调用的休眠事件侦听器期间,我有一个类似的用例。
解决方案是将要持久化的代码包装到另一个用REQUIRES_NEW
注释的方法中。 (在另一个类中)这样会产生一个新事务,并在方法返回后发出刷新/提交。
请记住,这可能会影响所有其他测试!所以相应地编写它们,否则您需要确保在测试运行后可以清理。
【讨论】:
太棒了。没想到。必须稍微分解一下我的测试方法,这让我不太兴奋,但效果很好。 @MartinFrey 为什么“在另一个班级”? 由于 spring 与代理一起工作以实现此功能(还有很多其他功能),您必须使用另一个类,以便 spring 能够触发事务。一旦你进入一个班级,你就不会再通过代理了。 这可以是内部类吗? 可以,只要是春豆。没有像这样测试它。试一试:)【参考方案2】:为什么不使用 spring 的TransactionTemplate
以编程方式控制事务?您还可以重组您的代码,以便每个“事务块”都有自己的 @Transactional
方法,但鉴于这是一个测试,我会选择对您的事务进行编程控制。
还要注意,runnable 上的 @Transactional
注释将不起作用(除非您使用 aspectj),因为 runnable 不是由 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 的回答)。 就我而言,它仅适用于transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
参见代码here【参考方案3】:
我知道由于TransactionTemplate
这种丑陋的匿名内部类用法看起来不太好,但是当出于某种原因我们想要一个测试方法时,恕我直言,它是最灵活的选项。
在某些情况下(取决于应用程序类型),在 Spring 测试中使用事务的最佳方式是在测试方法上关闭 @Transactional
。为什么?因为@Transactional
可能会导致很多假阳性测试。您可以查看此sample article 以了解详细信息。在这种情况下,TransactionTemplate
可以完美地控制我们想要控制的事务边界。
【讨论】:
您好,我想知道这是否成立:在对保存对象的语句创建集成测试时,建议刷新实体管理器以避免任何误报,即避免测试运行良好,但在生产中运行时其操作会失败。事实上,测试可能运行良好,因为第一级缓存没有刷新并且没有写入数据库。为了避免这种假阴性集成测试,请在测试主体中使用显式刷新。 @StephaneEybert IMO 只是刷新EntityManager
更像是一种 hack :-) 但这取决于您的需求和您正在进行的测试类型。对于真正的集成测试(应该与生产中的行为完全相同),答案是:绝对不要在测试周围使用@Transactional
。但缺点也存在:您必须在每次此类测试之前将数据库设置为众所周知的状态。以上是关于如何在@Transactional 方法中手动强制提交? [复制]的主要内容,如果未能解决你的问题,请参考以下文章
您如何测试 Spring @Transactional 而不只是命中休眠级别 1 缓存或进行手动会话刷新?
我们如何使用 jdbi 在 Dropwizard 中强制资源(控制器)级别的事务?
如果没有AspectJ,Spring中如何使用SpringAOP@Transactional?