You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.27 07:55:03