Java JPA @ManyToMany关联:MySQL中间关联表为空问题解决
多对多关联中间表
offer_skill_set为空的解决方法 问题背景
开发招聘信息下载应用,从在线服务获取JSON格式招聘数据,解析为自定义Offer类后存入MySQL。Offer与Skill为@ManyToMany关联,对应中间表offer_skill_set。当前运行状态:
- Skill表无重复数据,Offer表数据正常
- 中间表
offer_skill_set始终为空 - 调整
CascadeType后问题仍未解决,日志无报错
核心原因
JPA多对多关联中,仅关联拥有方(配置@JoinTable的一方)的操作会同步到中间表。当前代码中:
Skill类是关联拥有方(@JoinTable定义在offerSkillSet字段)Offer类的skillSet是被拥有方(通过mappedBy="offerSkillSet"声明)- 代码仅给
Offer设置了skillSet集合,但未同步更新Skill的offerSkillSet集合,导致JPA无法生成中间表关联数据
解决步骤
1. 调整关联拥有方(推荐)
从业务逻辑来看,招聘信息关联技能更符合操作习惯,将Offer设为关联拥有方:
修改Skill类的关联配置
@Getter @Setter @AllArgsConstructor @NoArgsConstructor @Entity @Table(name = "skill") public class Skill { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // 修改为被拥有方,指向Offer的skillSet字段 @JsonIgnore @ManyToMany(mappedBy = "skillSet") private Set<Offer> offerSkillSet = new HashSet<>(); @JsonIgnore @ManyToMany( fetch = FetchType.EAGER, cascade = CascadeType.MERGE, mappedBy = "skillPreferences") private Set<UserPreferences> userPreferencesSkillSet = new HashSet<>(); public Skill(String name) { this.name = name; } }
修改Offer类的关联配置
@Getter @Setter @Entity @Table(name = "offer") @NoArgsConstructor public class Offer { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long offerId; private String companyName; private boolean isRemoteWork; @JsonIgnore @OneToOne( cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinColumn(name = "salary_id") private OfferSalary offerSalary; private String location; private String url; // 设为关联拥有方,配置中间表信息 @JsonIgnore @ManyToMany( fetch = FetchType.EAGER, cascade = {CascadeType.MERGE, CascadeType.PERSIST} ) @JoinTable( name="offer_skill_set", joinColumns = @JoinColumn(name="offer_id"), inverseJoinColumns = @JoinColumn(name="skill_id"), uniqueConstraints = @UniqueConstraint(columnNames = {"offer_id", "skill_id"}) ) private Set<Skill> skillSet = new HashSet<>(); @JsonIgnore @ManyToMany(mappedBy = "offerPreferences") private Set<UserPreferences> userPreferencesSet = new HashSet<>(); public Offer(String companyName, boolean isRemoteWork, OfferSalary offerSalary, String location, Set<Skill> skillSet, String url) { this.companyName = companyName; this.isRemoteWork = isRemoteWork; this.offerSalary = offerSalary; this.location = location; this.skillSet = skillSet; this.url = url; } // 添加双向关联维护方法 public void addSkill(Skill skill) { this.skillSet.add(skill); skill.getOfferSkillSet().add(this); } }
2. 维护双向关联关系
修改JustJoinItDownloader的map方法,使用addSkill方法替代直接设置skillSet,确保双向关联同步:
@Override public List<Offer> map(String json) throws JsonProcessingException { List<JustJoinItRootPojo> jjitOffers = Arrays.stream(objectMapper.readValue(json, JustJoinItRootPojo[].class)).toList(); int size = jjitOffers.size(); List<Offer> offerListJjit = new ArrayList<>(); int counter = 1; for (JustJoinItRootPojo justJoinItRootPojo : jjitOffers) { System.out.println("mapowanie jjit: " + counter++ + "/" + size); for (OfferSalary offerSalary : getSalaryAndTypeOfContractJjit(justJoinItRootPojo)) { // 初始化空的skillSet,后续逐个添加 Offer offer = new Offer(justJoinItRootPojo.getCompany_name(), justJoinItRootPojo.isRemote(), offerSalary, justJoinItRootPojo.getCity(), new HashSet<>(), justJoinItRootPojo.getCompany_url()); // 获取技能集合 Set<Skill> skills = getSkillSetJjit(justJoinItRootPojo.getSkills() .stream().map(Skill::getName).collect(Collectors.toSet())); // 维护双向关联 skills.forEach(offer::addSkill); offerListJjit.add(offer); } } return offerListJjit; }
3. 优化Skill保存逻辑(可选)
去掉getSkillSetJjit中的saveAndFlush,改为在事务结束时统一保存,减少数据库交互:
private Set<Skill> getSkillSetJjit(Set<String> skills) { Set<Skill> skillSet = new HashSet<>(); for (String skill : skills) { Optional<Skill> skillByName = skillRepository.findByName(skill); if (skillByName.isPresent()) { skillSet.add(skillByName.get()); } else { skillSet.add(new Skill(skill)); } } return skillSet; }
由于Offer的skillSet配置了CascadeType.PERSIST,新创建的Skill会在Offer保存时被自动持久化。
验证
执行/test/offer-download-save接口后,检查:
- Offer表数据正常
- Skill表无重复数据
- 中间表
offer_skill_set生成对应的关联记录
内容的提问来源于stack exchange,提问作者Mrcnsb
相关产品推荐
相关产品推荐

