本文详解 Spring Boot 中使用 @DataJpaTest 进行 JPA Repository 单元测试时,因手动设置自增主键 ID 导致的 EntityNotFoundException 问题,并提供正确初始化测试数据、处理双向关联及确保数据库一致性的一站式解决方案。
本文详解 spring boot 中使用 `@datajpatest` 进行 jpa repository 单元测试时,因手动设置自增主键 id 导致的 `entitynotfoundexception` 问题,并提供正确初始化测试数据、处理双向关联及确保数据库一致性的一站式解决方案。
在 Spring Boot 的 JUnit 5 集成测试中,@DataJpaTest 是验证 Repository 层逻辑的黄金标准——它自动配置内存型 H2 数据库、启用 JPA 自动建表,并注入 TestEntityManager 和 @Autowired 的 Repository 实例。然而,一个常见却隐蔽的陷阱是:手动为 @GeneratedValue(strategy = GenerationType.IDENTITY) 字段赋值(如 domain.setId(1)),会导致 Hibernate 忽略数据库生成的真实 ID,进而引发级联操作失败或实体查找异常。
从你提供的日志和堆栈可知,错误核心在于:
Unable to find com.testiqo.exam.engine.entity.Domain with id 1 Caused by: jakarta.persistence.EntityNotFoundException
这并非数据库未建表或连接失败,而是 Topic 实体中持有的 Domain 引用(topic.setDomain(domain))仍为瞬态(transient)对象——其 id=1 是硬编码的,但 H2 实际插入后分配的是 id=1(或更大值),而 Hibernate 在 topicRepository.save(topic) 时尝试通过 domain_id 关联该 Domain,却发现数据库中并无 id=1 的记录(因 domainRepository.save(domain) 返回的是新 ID,但原始 domain 对象未更新)。
✅ 正确做法:让 JPA 管理 ID,依赖持久化后的实体引用
1. 移除所有手动 ID 赋值
// ❌ 错误:强制设置自增 ID(Hibernate 将忽略并生成新 ID)
private Domain createDomain() {
Domain domain = new Domain();
domain.setId(1); // ← 删除这一行!
domain.setName("Java");
domain.setUid("1234z637-c6c1-4dc3-9755-9f6ad974d70b");
return domain;
}
2. 在 @BeforeEach 中完成完整持久化链
确保 Domain 先保存,并将返回的托管实体(managed entity) 用于构造 Topic:
@BeforeEach
void setUp() {
// ✅ 先保存 Domain,获取数据库分配的真实 ID
domain = domainRepository.save(createDomain());
System.out.println("Saved Domain ID: " + domain.getId()); // 可选调试
// ✅ 使用已持久化的 domain 实例构建 Topic
topic = createTopic(domain); // 传入托管 domain
topic = topicRepository.save(topic);
System.out.println("Saved Topic ID: " + topic.getId());
}
private Domain createDomain() {
Domain domain = new Domain();
domain.setName("Java");
domain.setUid("1234z637-c6c1-4dc3-9755-9f6ad974d70b");
return domain;
}
private Topic createTopic(Domain domain) {
Topic topic = new Topic();
topic.setName("Java");
topic.setUid("35f66eaa-2fc7-4170-8470-feb83ddbf6b0");
topic.setDomain(domain); // ✅ 关联已托管的 domain
return topic;
}
3. 清理测试方法中的冗余保存操作
每个 @Test 方法内不再重复调用 save() —— 因为 @BeforeEach 已确保数据就绪:
@Test
void findAllTopicByDomainId() {
// ✅ 直接使用已持久化的 domain.uid 查询
List<topic> result = topicRepository.findAllTopicByDomainId(domain.getUid());
assertThat(result).hasSize(1)
.extracting("name")
.containsExactly("Java");
}
@Test
void deleteByUid() {
// ✅ 验证存在性(注意:findById 使用主键,不是 uid)
assertTrue(topicRepository.findById(topic.getId()).isPresent());
// ✅ 执行删除
topicRepository.deleteByUid(topic.getUid());
// ✅ 验证已删除
assertFalse(topicRepository.findById(topic.getId()).isPresent());
}</topic>
⚠️ 关键注意事项
- @DataJpaTest 默认启用事务回滚:每个测试方法在独立事务中运行,结束后自动回滚,无需手动清理数据。
- 避免 @MockBean 混用:@DataJpaTest 专为真实 JPA 测试设计,若同时使用 @MockBean 注入 Repository,将绕过数据库,失去集成测试意义。
- @AutoConfigureTestDatabase(replace = Replace.NONE) 风险:此配置会禁用 H2 自动配置,可能导致连接失败。建议移除此行,让 Spring Boot 自动配置内存数据库。
- 实体关系映射检查:确认 Topic 类中 @ManyToOne 关系是否正确定义(例如 @JoinColumn(name = "domain_id")),否则外键约束可能失效。
-
编译参数优化:解决日志中 Using deprecated '-debug' fallback 警告,在 build.gradle 中添加:
tasks.withType(JavaCompile) { options.compilerArgs
✅ 最终推荐测试结构
@DataJpaTest
@ActiveProfiles("test") // 替代 Constant.TEST_PROFILE(更符合 Spring 约定)
class TopicRepositoryTest {
@Autowired private TestEntityManager entityManager; // 可选:用于刷新/同步状态
@Autowired private TopicRepository topicRepository;
@Autowired private DomainRepository domainRepository;
private Domain domain;
private Topic topic;
@BeforeEach
void setUp() {
domain = domainRepository.save(createDomain());
topic = topicRepository.save(createTopic(domain));
}
// ... 测试方法(无需再 save/delete setup 数据)
}
遵循以上实践,你的 Repository 测试将稳定、可重现,并真实反映 JPA 在生产环境中的行为——既验证了查询逻辑,也保障了数据关联的完整性。











