Spring测试中如何实现事务在方法结束后统一回滚而非逐方法回滚?
在练习项目的集成测试中,给测试类添加了@Transactional注解以避免测试修改数据库,但发现每个JPA Repository方法执行后都会自动回滚。保存实体后查询对应数据返回空列表,因为保存操作已被回滚。希望将整个测试方法的逻辑包裹在同一个事务中,等测试方法执行完成后再统一回滚,而非逐方法单独回滚。
被测试的Service
@Service public class FridgeServiceImpl implements FridgeService { private final FridgeRepository fridgeRepository; public FridgeServiceImpl(FridgeRepository fridgeRepository) { this.fridgeRepository = fridgeRepository; } @Override public Fridge save(Fridge fridge) { return fridgeRepository.save(fridge); } @Override public List<IngredientInFridge> getAllInFridge(Long id) { return fridgeRepository.findAllInFridge(id); } @Override public List<IngredientInFridge> getAllToExpireAfterDate(LocalDate date) { return fridgeRepository.findAllWithExpirationDateAfter(date); } }
Repository
@Repository @Transactional public interface FridgeRepository extends JpaRepository<Fridge, Long> { @Query(value = "select i from Fridge f join f.fridgeContents i where i.id.fridgeId = :fridgeId") List<IngredientInFridge> findAllInFridge(@Param("fridgeId") Long fridgeId); @Query(value = "select i from IngredientInFridge i inner join IngredientGrammage ig on i.ingredient.id = ig.ingredient.id where i.fridge.id = :fridgeId " + "and ig.recipe.id = :recipeId") Set<IngredientInFridge> getIngredientsAvailableForRecipe(@Param("recipeId") Long recipeId, @Param("fridgeId") Long fridgeId); @Query(value = "select i from IngredientInFridge i where i.expirationDate <= :date") List<IngredientInFridge> findAllWithExpirationDateAfter(@Param("date") LocalDate date); }
测试类
@ExtendWith(SpringExtension.class) @ContextConfiguration(classes = {TestConfig.class}) @Transactional class FridgeServiceImplTest { @Autowired FridgeService fridgeService; Ingredient ingredient; Fridge fridge; IngredientInFridge ingredientInFridge; @BeforeEach public void setUp() { ingredient = new Ingredient(100, "Chicken", UnitOfMeasure.G); fridge = new Fridge(new HashSet<>()); ingredientInFridge = new IngredientInFridge(ingredient, fridge, 50, LocalDate.of(2020, 12, 30)); fridge.addToFridge(ingredientInFridge); } @Test void getAllInFridge_ThenReturn_1() { //given var savedFridge = fridgeService.save(fridge); //保存实体 //when var ingredientsInFridge = fridgeService.getAllInFridge(savedFridge.getId()); //因保存操作被回滚,返回空列表 //then Assertions.assertEquals(1, savedFridge.getFridgeContents().size()); Assertions.assertEquals(1, ingredientsInFridge.size()); //断言失败 } }
测试配置
@Configuration @EnableJpaRepositories(value = "com.calculate.calories.repository") @PropertySource("classpath:applicationTest.properties") @ComponentScan(basePackages = {"com.calculate.calories.service", "com.calculate.calories.repository"}) @EntityScan("com.calculate.calories.model") public class TestConfig { @Autowired private Environment environment; @Bean public LocalContainerEntityManagerFactoryBean entityManagerFactory() { LocalContainerEntityManagerFactoryBean em = new LocalContainerEntityManagerFactoryBean(); em.setDataSource(dataSource()); em.setPackagesToScan("com.calculate.calories.model"); JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter(); em.setJpaVendorAdapter(vendorAdapter); em.setJpaProperties(additionalProperties()); return em; } @Bean public DataSource dataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); dataSource.setDriverClassName(Objects.requireNonNull(environment.getProperty("spring.datasource.driver-class-name"))); dataSource.setUrl(Objects.requireNonNull(environment.getProperty("spring.datasource.url"))); dataSource.setUsername(Objects.requireNonNull(environment.getProperty("spring.datasource.username"))); dataSource.setPassword(Objects.requireNonNull(environment.getProperty("spring.datasource.password"))); return dataSource; } @Bean public PlatformTransactionManager transactionManager() { //JpaTransactionManager transactionManager = new JpaTransactionManager(); //transactionManager.setEntityManagerFactory(entityManagerFactory().getObject()); return new DataSourceTransactionManager(dataSource()); } @Bean public PersistenceExceptionTranslationPostProcessor exceptionTranslation() { return new PersistenceExceptionTranslationPostProcessor(); } Properties additionalProperties() { Properties properties = new Properties(); properties.setProperty("hibernate.hbm2ddl.auto", "none"); properties.setProperty("hibernate.dialect", "org.hibernate.dialect.MySQL5Dialect"); return properties; } }
已尝试的方案
- 用
JpaTransactionManager替换DataSourceTransactionManager:测试不会回滚,数据库被重复数据填满 - 将
@Transactional从类级别移到方法级别:无效果 - 保存后调用
EntityManager.flush():抛出javax.persistence.TransactionRequiredException: no transaction is in progress异常
1. 移除Repository上的@Transactional注解
Repository接口上的@Transactional会强制每个Repository方法开启独立事务并自动提交/回滚,与测试类的事务逻辑冲突。Spring Data JPA默认会给CRUD方法添加事务支持,自定义查询方法也可通过测试类的事务统一管理,因此直接删除Repository上的@Transactional即可。
2. 配置正确的JpaTransactionManager
测试配置中使用DataSourceTransactionManager无法正确管理JPA事务,必须改用JpaTransactionManager,同时确保测试类的@Transactional能触发事务回滚。修改transactionManager()方法:
@Bean public PlatformTransactionManager transactionManager() { JpaTransactionManager transactionManager = new JpaTransactionManager(); transactionManager.setEntityManagerFactory(entityManagerFactory().getObject()); return transactionManager; }
3. 确认测试事务的回滚行为
Spring Test的@Transactional默认会在测试方法结束后回滚事务,无需额外配置。若之前使用JpaTransactionManager时事务未回滚,检查测试类是否存在@Rollback(false)注解,或配置中是否修改了事务默认回滚规则。
4. (可选)显式控制事务提交/回滚
若需临时验证数据库数据,可在测试方法上添加@Commit,测试完成后改回默认逻辑即可。正常测试仅保留@Transactional注解即可。
验证修改后的逻辑
修改后,测试方法的整个流程会在同一个事务中执行:保存实体与查询操作都在事务内,查询能读到未提交的保存数据;测试结束后事务统一回滚,不会污染数据库。
内容的提问来源于stack exchange,提问作者Bart

