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 就级联删掉所有岗位,性能更好

七、避坑提醒

  1. 双向关系一定要写 mappedBy,否则会生成多余的中间表
  2. 保存数据时,两边都绑定关系(job.setCompany + company.getJobs().add),避免缓存数据不一致
  3. 生产环境不要随便用 CascadeType.ALL,尤其是删除操作,容易误删数据
  4. 优先用懒加载,避免 EAGER 带来的不必要查询

 

 

 

posted @ 2026-08-01 01:12  漫漫长路</>  阅读(2)  评论(0)    收藏  举报