You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 07:25:18