spring boot JPA 关系映射-----20

 

 

独立完整项目,聚焦「一对一用户档案、多对多岗位收藏、中间实体岗位投递」三大核心场景,严格遵循生产级最佳实践,复制即可启动运行。

一、项目整体结构

plaintext
 
 
com.example.jobapp
├── JobappApplication.java       # 启动类
├── entity
│   ├── SysUser.java             # 用户表
│   ├── UserProfile.java         # 用户档案(一对一)
│   ├── Job.java                 # 岗位表
│   └── JobApplication.java      # 投递记录(中间实体)
├── repository
│   ├── SysUserRepository.java
│   ├── UserProfileRepository.java
│   ├── JobRepository.java
│   └── JobApplicationRepository.java
├── service
│   └── JobAppService.java       # 业务逻辑封装
└── controller
    └── JobAppController.java    # REST接口

 

 

二、pom.xml 核心依赖

xml
 
 
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.0</version>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>jpa-relation-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

 

 

三、实体类(核心映射关系)

1. 用户主表 SysUser.java

对应 194、198、199 集:一对一档案 + 多对多收藏岗位
java
 
运行
 
 
package com.example.jobapp.entity;

import jakarta.persistence.*;
import lombok.Data;

import java.util.HashSet;
import java.util.Set;

@Data
@Entity
@Table(name = "sys_user")
public class SysUser {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false)
    private String username;

    private String email;

    /**
     * 一对一:用户 ↔ 用户档案
     * mappedBy = "user":外键在档案表维护,用户表不存外键
     * 级联保存/更新:操作用户时同步操作档案
     */
    @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
    private UserProfile profile;

    /**
     * 多对多:用户 ↔ 收藏的岗位
     * @JoinTable:指定中间表名和两个外键列
     * 用 Set 避免重复收藏;不设置级联删除,删用户不能删岗位
     * 对应 198、199 集 @ManyToMany 配置
     */
    @ManyToMany
    @JoinTable(
            name = "user_saved_jobs",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "job_id")
    )
    private Set<Job> savedJobs = new HashSet<>();
}
 

 

2. 用户档案表 UserProfile.java

对应 194 集:一对一关系的拥有方,存储简历、头像等扩展信息
java
 
运行
 
 
package com.example.jobapp.entity;

import jakarta.persistence.*;
import lombok.Data;

@Data
@Entity
@Table(name = "user_profile")
public class UserProfile {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String realName;    // 真实姓名
    private String phone;       // 手机号
    private String resumeUrl;   // 简历文件地址
    private String avatarUrl;   // 头像地址

    /**
     * 一对一:档案属于一个用户
     * @JoinColumn:外键列 user_id 存在这张表
     */
    @OneToOne
    @JoinColumn(name = "user_id", unique = true)
    private SysUser user;
}
 

 

3. 岗位表 Job.java

java
 
运行
 
 
package com.example.jobapp.entity;

import jakarta.persistence.*;
import lombok.Data;

@Data
@Entity
@Table(name = "job")
public class Job {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String jobTitle;
    private String company;
    private Integer salary;
    private String location;
}

 

 

4. 岗位投递记录 JobApplication.java

对应 201 集:有额外业务字段时,绝对不要用 @ManyToMany,改用独立中间实体
java
 
运行
 
 
package com.example.jobapp.entity;

import jakarta.persistence.*;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@Entity
@Table(name = "job_application")
public class JobApplication {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    /**
     * 多个投递记录 → 属于一个用户
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private SysUser user;

    /**
     * 多个投递记录 → 对应一个岗位
     */
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "job_id")
    private Job job;

    // ===== 额外业务字段:这就是不用 @ManyToMany 的原因 =====
    private String status;      // 投递状态:APPLIED-已投递 REVIEWED-已查看 OFFER-已录用 REJECTED-已拒绝
    private LocalDateTime applyTime; // 投递时间
    private String resumeVersion;   // 投递时使用的简历版本
}
 

 


四、Repository 层

java
 
运行
 
 
// SysUserRepository.java
package com.example.jobapp.repository;

import com.example.jobapp.entity.SysUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface SysUserRepository extends JpaRepository<SysUser, Long> {

    // 查询用户+收藏的岗位,解决N+1
    @EntityGraph(attributePaths = "savedJobs")
    Optional<SysUser> findWithSavedJobsById(Long id);
}

 

 
java
 
运行
 
 
// UserProfileRepository.java
package com.example.jobapp.repository;

import com.example.jobapp.entity.UserProfile;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface UserProfileRepository extends JpaRepository<UserProfile, Long> {
}

 

 
java
 
运行
 
 
// JobRepository.java
package com.example.jobapp.repository;

import com.example.jobapp.entity.Job;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public interface JobRepository extends JpaRepository<Job, Long> {
}

 

 
java
 
运行
 
 
// JobApplicationRepository.java
package com.example.jobapp.repository;

import com.example.jobapp.entity.JobApplication;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Optional;

@Repository
public interface JobApplicationRepository extends JpaRepository<JobApplication, Long> {

    // 查询用户的所有投递记录
    List<JobApplication> findByUserId(Long userId);

    // 查询用户是否投递过某岗位
    Optional<JobApplication> findByUserIdAndJobId(Long userId, Long jobId);
}

 

 

五、Service 业务层

对应 195、200、202 集:档案管理、收藏功能、投递流程
java
 
运行
 
 
package com.example.jobapp.service;

import com.example.jobapp.entity.Job;
import com.example.jobapp.entity.JobApplication;
import com.example.jobapp.entity.SysUser;
import com.example.jobapp.entity.UserProfile;
import com.example.jobapp.repository.JobApplicationRepository;
import com.example.jobapp.repository.JobRepository;
import com.example.jobapp.repository.SysUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;

@Service
@RequiredArgsConstructor
public class JobAppService {

    private final SysUserRepository userRepository;
    private final JobRepository jobRepository;
    private final JobApplicationRepository applicationRepository;

    // ========== 1. 用户档案管理(对应194、195集) ==========

    @Transactional(readOnly = true)
    public UserProfile getUserProfile(Long userId) {
        SysUser user = userRepository.findById(userId)
                .orElseThrow(() -> new RuntimeException("用户不存在"));
        return user.getProfile();
    }

    @Transactional(rollbackFor = Exception.class)
    public UserProfile saveOrUpdateProfile(Long userId, UserProfile profile) {
        SysUser user = userRepository.findById(userId)
                .orElseThrow(() -> new RuntimeException("用户不存在"));

        // 双向绑定
        profile.setUser(user);
        user.setProfile(profile);

        // 级联保存,只存用户即可自动保存档案
        SysUser saved = userRepository.save(user);
        return saved.getProfile();
    }

    // ========== 2. 岗位收藏功能(对应200集) ==========

    /**
     * 收藏/取消收藏岗位
     * 已收藏则取消,未收藏则添加
     */
    @Transactional(rollbackFor = Exception.class)
    public String toggleSaveJob(Long userId, Long jobId) {
        SysUser user = userRepository.findWithSavedJobsById(userId)
                .orElseThrow(() -> new RuntimeException("用户不存在"));
        Job job = jobRepository.findById(jobId)
                .orElseThrow(() -> new RuntimeException("岗位不存在"));

        Set<Job> savedJobs = user.getSavedJobs();
        if (savedJobs.contains(job)) {
            savedJobs.remove(job);
            return "已取消收藏";
        } else {
            savedJobs.add(job);
            return "收藏成功";
        }
    }

    /**
     * 查询我的收藏列表
     */
    @Transactional(readOnly = true)
    public Set<Job> getMySavedJobs(Long userId) {
        SysUser user = userRepository.findWithSavedJobsById(userId)
                .orElseThrow(() -> new RuntimeException("用户不存在"));
        return user.getSavedJobs();
    }

    // ========== 3. 岗位投递流程(对应202、203集) ==========

    /**
     * 投递岗位
     */
    @Transactional(rollbackFor = Exception.class)
    public String applyJob(Long userId, Long jobId, String resumeVersion) {
        // 校验是否已投递
        if (applicationRepository.findByUserIdAndJobId(userId, jobId).isPresent()) {
            return "请勿重复投递";
        }

        SysUser user = userRepository.getReferenceById(userId);
        Job job = jobRepository.getReferenceById(jobId);

        JobApplication application = new JobApplication();
        application.setUser(user);
        application.setJob(job);
        application.setStatus("APPLIED");
        application.setApplyTime(LocalDateTime.now());
        application.setResumeVersion(resumeVersion);

        applicationRepository.save(application);
        return "投递成功";
    }

    /**
     * 撤回投递
     */
    @Transactional(rollbackFor = Exception.class)
    public String withdrawApplication(Long applyId) {
        JobApplication app = applicationRepository.findById(applyId)
                .orElseThrow(() -> new RuntimeException("投递记录不存在"));

        if (!"APPLIED".equals(app.getStatus())) {
            return "当前状态不可撤回";
        }

        applicationRepository.delete(app);
        return "已撤回投递";
    }

    /**
     * 查询我的投递记录
     */
    @Transactional(readOnly = true)
    public List<JobApplication> getMyApplications(Long userId) {
        return applicationRepository.findByUserId(userId);
    }
}
 

 


六、Controller 接口层

对应 196、197、203 集:完整 REST 接口
java
 
运行
 
 
package com.example.jobapp.controller;

import com.example.jobapp.entity.Job;
import com.example.jobapp.entity.JobApplication;
import com.example.jobapp.entity.UserProfile;
import com.example.jobapp.service.JobAppService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Set;

@RestController
@RequestMapping("/api")
@RequiredArgsConstructor
public class JobAppController {

    private final JobAppService jobAppService;

    // ===== 用户档案接口 =====
    @GetMapping("/profile/{userId}")
    public UserProfile getProfile(@PathVariable Long userId) {
        return jobAppService.getUserProfile(userId);
    }

    @PostMapping("/profile/{userId}")
    public UserProfile saveProfile(@PathVariable Long userId,
                                   @RequestBody UserProfile profile) {
        return jobAppService.saveOrUpdateProfile(userId, profile);
    }

    // ===== 岗位收藏接口 =====
    @PostMapping("/save-job/{userId}/{jobId}")
    public String toggleSaveJob(@PathVariable Long userId,
                                @PathVariable Long jobId) {
        return jobAppService.toggleSaveJob(userId, jobId);
    }

    @GetMapping("/save-job/{userId}")
    public Set<Job> getSavedJobs(@PathVariable Long userId) {
        return jobAppService.getMySavedJobs(userId);
    }

    // ===== 岗位投递接口 =====
    @PostMapping("/apply/{userId}/{jobId}")
    public String applyJob(@PathVariable Long userId,
                           @PathVariable Long jobId,
                           @RequestParam String resumeVersion) {
        return jobAppService.applyJob(userId, jobId, resumeVersion);
    }

    @DeleteMapping("/apply/{applyId}")
    public String withdraw(@PathVariable Long applyId) {
        return jobAppService.withdrawApplication(applyId);
    }

    @GetMapping("/apply/{userId}")
    public List<JobApplication> getApplications(@PathVariable Long userId) {
        return jobAppService.getMyApplications(userId);
    }
}

 

 

七、启动类 + 配置

java
 
运行
 
 
package com.example.jobapp;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class JobappApplication {
    public static void main(String[] args) {
        SpringApplication.run(JobappApplication.class, args);
    }
}
 
application.properties

 

properties
 
 
spring.datasource.url=jdbc:mysql://localhost:3306/jobapp?serverTimezone=Asia/Shanghai&useSSL=false
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

 

 

八、核心最佳实践(对应 201 集)

什么时候用 @ManyToMany?

✅ 只有纯关联,没有任何额外字段时使用,比如「用户收藏岗位」,只有用户和岗位的绑定关系,没有其他属性。

什么时候绝对不能用 @ManyToMany?

❌ 中间表有业务字段时,必须拆成独立实体,比如「岗位投递」有投递时间、投递状态、简历版本等字段,用独立实体扩展性更强。

其他通用原则

  1. 多对多用 Set 不用 List,避免重复数据、提升性能
  2. 关联查询用 @EntityGraph 解决 N+1 问题
  3. 多对多不要设置 CascadeType.ALL,删除用户不能把岗位也删了
  4. 一对一外键放在从属表,主表用 mappedBy 维护关系
  5. 懒加载作为默认策略,需要关联数据时再主动抓取

九、课程对应表

表格
 
集数核心内容对应代码
194 用户档案库表与实体设计 SysUser + UserProfile 一对一映射
195 档案、简历、图片上传接口 saveOrUpdateProfile 方法
196 档案详情查询接口 getUserProfile 方法
197 档案管理完整演示 档案全套接口
198 @ManyToMany 原理讲解 SysUser 中 savedJobs 多对多设计
199 @ManyToMany 实体配置 @JoinTable 中间表配置
200 收藏 / 取消收藏接口 toggleSaveJob 方法
201 什么时候不用 @ManyToMany JobApplication 独立中间实体
202 岗位投递工作流实现 applyJob、withdrawApplication 方法
203 投递与撤回接口演示 投递全套 REST 接口
posted @ 2026-08-01 02:29  漫漫长路</>  阅读(5)  评论(0)    收藏  举报