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

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也是同一个原因:事务没提交,实体生命周期回调生成的字段值还没完成持久化同步,数据库里的字段自然是空值,只是因为接口返回不包含这两个字段,所以没有抛异常。
解决方案

修复可以选两种常用方案:

  1. 给测试方法加@Commit注解(或等价的@Rollback(false))
    配置后测试方法的事务会在执行完成后正常提交,提交过程会自动触发flush,生成update SQL、执行@PreUpdate逻辑。注意这种方式会把测试数据真实写入数据库,建议搭配@DirtiesContext等测试上下文清理注解使用,避免脏数据干扰其他测试用例。
  2. 手动触发flush,保留自动回滚能力
    如果不想在测试库残留数据,可以在调用完更新接口后,注入EntityManager调用entityManager.flush(),手动强制把持久化上下文的修改同步到数据库,触发update SQL和@PreUpdate逻辑,测试结束后事务依然会自动回滚,不会产生脏数据。

不建议直接删掉测试类上的@Transactional注解,否则每个接口的事务独立提交,会在测试库残留大量测试数据,提升维护成本。

内容的提问来源于stack exchange,提问作者Melod_Yi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 00:03:34