本文详解 Spring Boot + JUnit5 + H2 测试环境下,因手动设置自增主键 ID 导致 EntityNotFoundException 的根本原因,并提供安全、可复用的测试数据初始化方案。
本文详解 spring boot + junit5 + h2 测试环境下,因手动设置自增主键 id 导致 `entitynotfoundexception` 的根本原因,并提供安全、可复用的测试数据初始化方案。
在使用 @DataJpaTest 配合 H2 内存数据库进行 Repository 层单元测试时,一个常见但极易被忽视的问题是:手动为 @GeneratedValue(strategy = GenerationType.IDENTITY) 字段赋值(如 domain.setId(1))会导致数据库实际生成的 ID 与 Java 对象中持有的 ID 不一致,进而引发级联操作失败或 EntityNotFoundException。
从你提供的日志中可清晰看到关键线索:
Hibernate: insert into domain (id, name, uid) values (default, ?, ?) ... Unable to find com.testiqo.exam.engine.entity.Domain with id 1
H2 正确执行了 INSERT INTO domain (...) VALUES (default, ...) —— 即让数据库自动生成 ID(例如 1、2 等),但你的 Domain 实体对象中 id=1 是硬编码的。当 Topic 关联该 Domain 并调用 topicRepository.save(topic) 时,JPA 尝试根据 topic.getDomain().getId()(即 1)去加载 Domain,而此时若该 ID 尚未被持久化(或已被覆盖/冲突),就会抛出 EntityNotFoundException。
✅ 正确做法:让数据库生成 ID,再回填实体
@BeforeEach 方法应负责完整生命周期管理:创建 → 持久化 → 获取真实 ID → 构建依赖关系。避免在测试方法内重复 save(),更不可在 createTopic() 中新建 Domain(造成对象不一致)。
以下是重构后的推荐写法:
@DataJpaTest
@ActiveProfiles(Constant.TEST_PROFILE)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 显式禁用自动配置,确保使用 H2
class TopicRepositoryTest {
@Autowired
private TestEntityManager entityManager; // 推荐使用,比直接 save() 更可控
@Autowired
private TopicRepository topicRepository;
@Autowired
private DomainRepository domainRepository;
private Domain domain;
private Topic topic;
@BeforeEach
void setUp() {
// 1. 创建并持久化 Domain,获取数据库分配的真实 ID
domain = createDomain();
domain = domainRepository.save(domain); // ← 关键:保存后 domain.getId() 已更新为 DB 生成值
System.out.println("Saved Domain ID: " + domain.getId());
// 2. 创建 Topic,关联已持久化的 domain 实例(非新构造对象)
topic = createTopic(domain);
topic = topicRepository.save(topic);
System.out.println("Saved Topic ID: " + topic.getId());
}
private Domain createDomain() {
Domain d = new Domain();
d.setName("Java");
d.setUid("1234z637-c6c1-4dc3-9755-9f6ad974d70b");
return d;
}
private Topic createTopic(Domain domain) {
Topic t = new Topic();
t.setName("Java");
t.setUid("35f66eaa-2fc7-4170-8470-feb83ddbf6b0");
t.setDomain(domain); // ← 直接引用已保存的 domain 实例
return t;
}
@Test
void findAllTopicByDomainId() {
// domain 和 topic 已在 @BeforeEach 中保存,无需重复 save
List<Topic> result = topicRepository.findAllTopicByDomainId(domain.getUid());
assertThat(result).hasSize(1)
.extracting("name").containsExactly("Java");
}
@Test
void deleteByUid() {
assertTrue(topicRepository.findById(topic.getId()).isPresent());
topicRepository.deleteByUid(topic.getUid());
assertFalse(topicRepository.findById(topic.getId()).isPresent());
}
@Test
void getTopicByUid() {
Topic result = topicRepository.getTopicByUid(topic.getUid());
assertEquals("Java", result.getName());
assertEquals(domain.getId(), result.getDomain().getId()); // 验证关联完整性
}
}⚠️ 关键注意事项
- 移除 @ExtendWith(MockitoExtension.class):@DataJpaTest 是集成测试注解,用于启动 JPA 上下文,与 Mockito 模拟无直接关系;混用可能导致上下文冲突,且 @MockBean 才是 JPA 测试中模拟依赖的正确方式(本例无需)。
- @AutoConfigureTestDatabase(replace = Replace.NONE) 已过时:Spring Boot 3+ 应使用 @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE),确保 H2 配置生效。
- 避免手动设 ID:所有 @GeneratedValue 字段(如 id)在测试中绝不应手动赋值,全部交由数据库生成。
- 优先使用 TestEntityManager:它提供 persistAndFlush()、find() 等精确控制方法,比直接调用 Repository 更适合验证底层行为。
- 验证关联完整性:如 getTopicByUid() 测试中检查 result.getDomain().getId(),确保外键关系正确建立。
遵循以上实践,即可彻底规避“ID 不匹配”导致的 JpaObjectRetrievalFailureException,写出稳定、可维护、符合 JPA 语义的 Repository 单元测试。

















