spring boot JPA 实体关系「一对多 / 多对一」---09
用你招聘平台最贴合的场景来演示:一个公司(Company)可以发布多个岗位(Job),反过来多个岗位属于同一个公司。这是 JPA 最常用、最核心的关系,完全对应第 9 节全部知识点。
一、前置说明
- 无需新增依赖,你项目里的
spring-boot-starter-data-jpa就够用 - 包路径和之前保持一致:
com.example.jobportal - 数据库表会由 Hibernate 自动创建(配合
ddl-auto=update)
二、核心实体类(带逐行注释)
1. 多方:岗位 Job(关系维护端,外键在这张表)
java
运行
package com.example.jobportal.entity; import jakarta.persistence.*; import lombok.Data; @Data @Entity @Table(name = "jobs") public class Job { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String jobTitle; // 岗位名称 private Integer salary; // 薪资 private String location; // 工作地点 /** * 多对一:多个岗位 → 属于一个公司 * @JoinColumn:指定外键列名 company_id,会自动生成在 jobs 表中 * fetch = FetchType.LAZY:懒加载,查岗位时不会立刻查公司详情,用到才查 */ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "company_id") private Company company; }
2. 一方:公司 Company(被关联端,用集合装多个岗位)
java
运行
package com.example.jobportal.entity; import jakarta.persistence.*; import lombok.Data; import org.hibernate.annotations.OnDelete; import org.hibernate.annotations.OnDeleteAction; import java.util.ArrayList; import java.util.List; @Data @Entity @Table(name = "companies") public class Company { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // 公司名称 private String address; // 公司地址 /** * 一对多:一个公司 → 有多个岗位 * mappedBy = "company":我不维护外键,一切以Job类里的company字段为准(必须写!) * cascade = CascadeType.ALL:增删改公司时,同步操作里面的岗位 * orphanRemoval = true:孤儿删除,把岗位从集合里移除,数据库也会删掉这条岗位 * @OnDelete:数据库层面级联删除,删公司时一条SQL删掉所有岗位(课程106重点) */ @OneToMany( mappedBy = "company", cascade = CascadeType.ALL, orphanRemoval = true ) @OnDelete(action = OnDeleteAction.CASCADE) private List<Job> jobs = new ArrayList<>(); }
三、Repository 接口
java
运行
// CompanyRepository.java package com.example.jobportal.repository; import com.example.jobportal.entity.Company; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface CompanyRepository extends JpaRepository<Company, Long> { }
java
运行
// JobRepository.java package com.example.jobportal.repository; import com.example.jobportal.entity.Job; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface JobRepository extends JpaRepository<Job, Long> { }
四、启动自动插入测试数据(对应课程 102)
项目启动时自动创建测试数据,不用手动写 SQL:
java
运行
package com.example.jobportal.config; import com.example.jobportal.entity.Company; import com.example.jobportal.entity.Job; import com.example.jobportal.repository.CompanyRepository; import lombok.RequiredArgsConstructor; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component; @Component @RequiredArgsConstructor public class InitDataLoader implements CommandLineRunner { private final CompanyRepository companyRepository; @Override public void run(String... args) { // 有数据就不重复插入 if (companyRepository.count() > 0) return; // 1. 创建公司 Company company = new Company(); company.setName("字节跳动"); company.setAddress("北京海淀区"); // 2. 创建2个岗位 Job job1 = new Job(); job1.setJobTitle("Java后端开发"); job1.setSalary(25000); job1.setLocation("北京"); job1.setCompany(company); // 维护关系:岗位绑定公司 Job job2 = new Job(); job2.setJobTitle("前端开发"); job2.setSalary(22000); job2.setLocation("上海"); job2.setCompany(company); // 3. 双向绑定:把岗位加入公司集合 company.getJobs().add(job1); company.getJobs().add(job2); // 4. 只保存公司,因为级联ALL,岗位会自动存入数据库 companyRepository.save(company); System.out.println("✅ 测试数据插入完成"); } }
五、测试接口(方便你验证效果)
java
运行
package com.example.jobportal.controller; import com.example.jobportal.entity.Company; import com.example.jobportal.repository.CompanyRepository; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/api/companies") @RequiredArgsConstructor public class CompanyController { private final CompanyRepository companyRepository; @GetMapping("/{id}") public Company getCompanyDetail(@PathVariable Long id) { Company company = companyRepository.findById(id).orElseThrow(); // 调用 getJobs() 才会触发懒加载,去数据库查岗位 System.out.println("该公司岗位数:" + company.getJobs().size()); return company; } }
启动项目后访问:
plaintext
http://localhost:8080/api/companies/1
就能看到公司信息 + 它下面的所有岗位。
六、课程核心知识点大白话解释
1. mappedBy 是什么?(必懂)
双向关系里,只能有一方管外键,我们规定「多方(Job)管外键」。
mappedBy = "company" 翻译成人话就是:我 Company 这边不维护外键,外键怎么存、怎么改,全听 Job 类里的company属性。不写 mappedBy 的话,JPA 会自动生成一张中间表,就不是我们想要的「外键关联」了。
2. Fetch 抓取策略(课程 105)
决定「查数据的时候,关联数据要不要一起查出来」
- LAZY 懒加载(推荐):查公司时,不查岗位;只有你调用
getJobs()时,才发 SQL 去查。省性能,默认值。 - EAGER 立即加载:查公司时,立刻把所有岗位一起查出来。容易导致性能爆炸、N+1 问题,不建议用。
@ManyToOne默认是 EAGER,我们手动改成 LAZY 是行业最佳实践。
3. Cascade 级联操作(课程 105)
翻译成人话:操作爸爸的时候,要不要顺带操作儿子
PERSIST:保存公司时,顺带保存里面的岗位REMOVE:删除公司时,顺带删除里面的岗位ALL:增删改全部级联orphanRemoval = true:孤儿删除。把岗位从公司集合里移除,数据库里这条岗位也会被删掉。
4. @OnDelete 是什么?(课程 106)
和
CascadeType.REMOVE 效果类似,但实现层级不一样:CascadeType.REMOVE:JPA 层面,先查所有岗位,一条一条删,发很多条 SQL@OnDelete(CASCADE):数据库层面,删公司时一条 SQL 就级联删掉所有岗位,性能更好
七、避坑提醒
- 双向关系一定要写
mappedBy,否则会生成多余的中间表 - 保存数据时,两边都绑定关系(
job.setCompany+company.getJobs().add),避免缓存数据不一致 - 生产环境不要随便用
CascadeType.ALL,尤其是删除操作,容易误删数据 - 优先用懒加载,避免 EAGER 带来的不必要查询

浙公网安备 33010602011771号