SpringBoot MockMVC集成JPA测试 update无SQL @PreUpdate失效
问题描述
我基于spring-data-jpa与spring-boot-web搭建了演示应用,尝试使用MockMVC编写集成测试用例:依次调用新增接口、修改接口、查询接口,校验数据修改是否生效。我配置了spring.jpa.properties.hibernate.show_sql参数用于在控制台打印SQL日志,但测试运行时控制台仅输出1条insert SQL、1条查询SQL,未生成对应的update SQL;同时我配置的@PreUpdate注解未生效,代码覆盖率检测显示该注解标注的方法从未被执行。
但使用Postman手动调用接口测试时功能完全符合预期:控制台可正常输出update SQL,@PreUpdate注解标注的逻辑也正常执行。
断点调试显示内存中数据确实被修改,但create_time、update_time字段被置为null(即使我已为字段配置nullable = false约束),测试运行未抛出任何异常(因接口返回字段不包含updateTime等字段)。
控制台打印的SQL日志如下:
Hibernate: insert into account_item (create_time, update_time, name, total_money, transaction_time) values (?, ?, ?, ?, ?) Hibernate: select count(*) as col_0_0_ from account_item accountite0_ where accountite0_.id=?
相关代码
测试类代码
@ExtendWith(SpringExtension.class) @AutoConfigureMockMvc(addFilters = false) @SpringBootTest @Transactional class TechyoControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @SneakyThrows @Test public void should_update_success() { String createRequest = "{\"name\":\"name_test\",\"money\":12.3}"; MvcResult mvcCreateResult = mockMvc .perform(MockMvcRequestBuilders.post("/accountItems") .contentType(MediaiaType.APPLICATION_JSON) .content(createRequest)) .andReturn(); AccountItemResponse createResult = objectMapper.readValue(mvcCreateResult.getResponse().getContentAsString(), AccountItemResponse.class); // update String updateRequest = "{" + " \"name\": \"updated_name\"," + " \"date\": " + createResult.getTransactionTime().getTime() + "," + " \"lines\": [{" + " \"name\": \"line1\"," + " \"money\": 12" + " }, {\n" + " \"name\": \"line1\"," + " \"money\": 12" + " }, {\n" + " \"name\": \"line1\"," + " \"money\": -12" + " }]" + "}"; MvcResult mvcUpdateResult = mockMvc .perform(MockMvcRequestBuilders.put("/accountItems/" + createResult.getId()) .contentType(MediaType.APPLICATION_JSON) .content(updateRequest)) .andExpect(status().isOk()) .andReturn(); AccountItemResponse updateResult = objectMapper.readValue(mvcUpdateResult.getResponse().getContentAsString(), AccountItemResponse.class); assertThat(updateResult.getId()).isEqualTo(createResult.getId()); assertThat(updateResult.getTransactionTime()).hasSameTimeAs(createResult.getTransactionTime()); assertThat(updateResult.getTotal()).isEqualTo(new BigDecimal("12.00")); assertThat(updateResult.getAccountItemLines()).hasSize(3); // query MvcResult mvcGetResult = mockMvc .perform(MockMvcRequestBuilders.get("/accountItems/" + createResult.getId())) .andExpect(status().isOk()) .andReturn(); AccountItemResponse getResult = objectMapper.readValue(mvcGetResult.getResponse().getContentAsString(), AccountItemResponse.class); assertThat(getResult.getId()).isEqualTo(createResult.getId()); assertThat(getResult.getTransactionTime()).hasSameTimeAs(createResult.getTransactionTime()); assertThat(getResult.getTotal()).isEqualTo(new BigDecimal("12.00")); assertThat(getResult.getAccountItemLines()).hasSize(3); } }
实体基类代码
@MappedSuperclass @Getter @Setter @RequiredArgsConstructor @Accessors(chain = true) public class BasePO { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(updatable = false, nullable = false) @Temporal(TemporalType.TIMESTAMP) private Date createTime; @Column(nullable = false) @Temporal(TemporalType.TIMESTAMP) private Date updateTime; @PrePersist protected void prePersist() { Date now = new Date(); if (createTime == null) { createTime = now; } if (updateTime == null) { updateTime = now; } } @PreUpdate protected void preUpdate() { updateTime = new Date(); } @PreRemove protected void preRemove() { updateTime = new Date(); } }
业务实体类代码
@Entity @Table(name = "account_item") @Accessors(chain = true) @Getter @Setter @RequiredArgsConstructor public class AccountItemPO extends BasePO { @Column(nullable = false) private String name; @Column(nullable = false, columnDefinition = "decimal(10,2)") private BigDecimal totalMoney; private Date transactionTime; }
问题原因
核心问题是测试类上的@Transactional注解导致的:
- Spring测试环境中,标注在测试类/方法上的
@Transactional默认会在测试执行完毕后自动回滚所有操作,且整个测试方法运行在同一个外层事务上下文内。 - 测试中依次调用新增、修改、查询三个接口时,每个接口本身的独立事务会被Spring Test的事务拦截器绑定到测试方法的外层事务上,不会在单个接口请求结束时立即提交。
- JPA的update SQL生成、
@PreUpdate回调触发都发生在事务提交前的flush阶段。由于所有操作都在同一个未提交的事务中,后续查询直接命中Hibernate一级缓存(持久化上下文)返回内存中的修改后数据,不需要触发flush把修改同步到数据库,自然不会生成update SQL,@PreUpdate标注的方法也不会被执行。 - 用Postman手动调用时每个请求都是独立事务,请求处理完事务立即提交,会正常触发flush流程,所以update SQL、
@PreUpdate逻辑都能正常执行。 - 断点看到的create_time、update_time为null也是同一个原因:事务没提交,实体生命周期回调生成的字段值还没完成持久化同步,数据库里的字段自然是空值,只是因为接口返回不包含这两个字段,所以没有抛异常。
解决方案
修复可以选两种常用方案:
- 给测试方法加
@Commit注解(或等价的@Rollback(false))
配置后测试方法的事务会在执行完成后正常提交,提交过程会自动触发flush,生成update SQL、执行@PreUpdate逻辑。注意这种方式会把测试数据真实写入数据库,建议搭配@DirtiesContext等测试上下文清理注解使用,避免脏数据干扰其他测试用例。 - 手动触发flush,保留自动回滚能力
如果不想在测试库残留数据,可以在调用完更新接口后,注入EntityManager调用entityManager.flush(),手动强制把持久化上下文的修改同步到数据库,触发update SQL和@PreUpdate逻辑,测试结束后事务依然会自动回滚,不会产生脏数据。
不建议直接删掉测试类上的
@Transactional注解,否则每个接口的事务独立提交,会在测试库残留大量测试数据,提升维护成本。
内容的提问来源于stack exchange,提问作者Melod_Yi
相关产品推荐
相关产品推荐

