JUnit 5 Repository 测试中实体 ID 冲突与级联保存问题详解

星晨酱_7745

星晨酱_7745

2026-06-14

408人浏览

原创

本文详解 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 对象未更新)。

FaceSwapper
FaceSwapper

FaceSwapper是一款支持照片、视频和 GIF 换脸的在线 AI 换脸工具。

下载

✅ 正确做法:让 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 在生产环境中的行为——既验证了查询逻辑,也保障了数据关联的完整性。

相关文章

PHP速学视频免费教程(入门到精通)
PHP速学视频免费教程(入门到精通)

PHP怎么学习?PHP怎么入门?PHP在哪学?PHP怎么学才快?不用担心,这里为大家提供了PHP速学教程(入门到精通),有需要的小伙伴保存下载就能学习啦!

下载

相关标签:

本站声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn

相关专题

更多
软件测试常用工具
软件测试常用工具

软件测试常用工具有Selenium、JUnit、Appium、JMeter、LoadRunner、Postman、TestNG、LoadUI、SoapUI、Cucumber和Robot Framework等等。测试人员可以根据具体的测试需求和技术栈选择适合的工具,提高测试效率和准确性 。

2023.10.13

4150

8

java测试工具有哪些
java测试工具有哪些

java测试工具有JUnit、TestNG、Mockito、Selenium、Apache JMeter和Cucumber。php还给大家带来了java有关的教程,欢迎大家前来学习阅读,希望对大家能有所帮助。

2023.10.23

2541

10

Java 单元测试
Java 单元测试

本专题聚焦 Java 在软件测试与持续集成流程中的实战应用,系统讲解 JUnit 单元测试框架、Mock 数据、集成测试、代码覆盖率分析、Maven 测试配置、CI/CD 流水线搭建(Jenkins、GitHub Actions)等关键内容。通过实战案例(如企业级项目自动化测试、持续交付流程搭建),帮助学习者掌握 Java 项目质量保障与自动化交付的完整体系。

2025.10.24

986

26

mybatis一级缓存和二级缓存
mybatis一级缓存和二级缓存

在MyBatis中,一级缓存和二级缓存是两种不同级别的缓存机制,它们都可以用来提高性能。本专题提供mybatis一级缓存和二级缓存相关文章,大家可以免费阅读。

2023.08.21

583

4

ibatis和mybatis有什么区别
ibatis和mybatis有什么区别

ibatis和mybatis的区别:1、基本信息不同;2、开发时间不同;3、功能与易用性;4、配置文件;5、入参类型与出参类型;6、返回结果集接受方式;7、语法差异;8、数据库方言支持;9、插件支持;10、社区活跃度;11、全球化支持。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.02.23

2082

5

mybatis如何配置数据库连接
mybatis如何配置数据库连接

mybatis配置数据库连接的方法:1、指定数据源;2、配置事务管理器;3、配置类型处理器和映射器;4、使用环境元素;5、配置别名。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.02.23

446

5

mybatis工作原理及流程是什么
mybatis工作原理及流程是什么

mybatis工作原理及流程:1、配置文件;2、接口与映射;3、sql解析与生成;4、执行计划;5、结果处理;6、动态sql;7、缓存机制;8、插件;9、事务管理;10、日志与监控;11、扩展性。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.02.23

292

4

hibernate和mybatis有哪些区别
hibernate和mybatis有哪些区别

hibernate和mybatis的区别:1、实现方式;2、性能;3、对象管理的对比;4、缓存机制。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

2024.02.23

302

5

Java MyBatis框架
Java MyBatis框架

本专题专注于Java主流ORM框架MyBatis的应用,系统讲解SQL映射、动态SQL、结果映射、分页查询、缓存机制与多表关联等核心内容,并结合企业管理系统、电商平台和后台管理项目实战,帮助学员全面掌握高效的数据库持久层开发技能。

2025.08.26

5169

22

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
热门推荐
/
最新课程
phpStudy极速入门视频教程
phpStudy极速入门视频教程

共6课时 | 54.6万人学习

独孤九贱(4)_PHP视频教程
独孤九贱(4)_PHP视频教程

共89课时 | 133.3万人学习