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

JPA一对多关联异常求助:Membre与Sortie关联未正确存储

JPA一对多关联问题:Sortie未关联到Membre导致断言失败

问题现象

在Membre与Sortie的JPA一对多关联场景中,执行测试时出现断言失败:org.opentest4j.AssertionFailedError: expected: <1> but was: <0>,即添加到Membre的Sortie未被持久化关联。

相关代码

Membre.java

@Entity(name = "Membre")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Membre implements Serializable {
    private static final long serialVersionUID = 1L;
    
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long idMembre ;
    
    @Basic(optional = false)
    private String nomMembre ;
    
    @Basic(optional = false)
    private String prenomMembre ;
    
    @Basic(optional = false)
    private String adresseElectronique ; 
    
    //Il est plus sure d'utiliser un char[] plutot qu'un String pour un mot de passe 
    @Basic(optional = false)
    private char[] motDePasse ;
    
    @Version
    private long versionMembre = 0 ;
    
    public Membre(String nom, String prenom, String email, char[] mdp, Set<Sortie> sorties){
        this(0, nom, prenom, email, mdp, 0, sorties);
    }
    
    @OneToMany(fetch = FetchType.LAZY,
            mappedBy = "createurSortie",
            cascade = { CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REMOVE })
    @ToString.Exclude
    @OrderBy("idSortie asc")
    private Set<Sortie> sortiesMembre ;
    
    public void addSortie(Sortie s) {
        if (sortiesMembre == null) {
            sortiesMembre = new HashSet<>();
        }
        sortiesMembre.add(s);
        s.setCreateurSortie(this);
    }
    
    @PreUpdate
    public void beforeUpdate() {
        System.err.println("PreUpdate␣of␣" + this);
    }
    @PostUpdate
    public void afterUpdate() {
        System.err.println("PostUpdate␣of␣" + this);
    }
}

Sortie.java

@Entity(name = "Sortie")
@Data
@NoArgsConstructor
@AllArgsConstructor
@NamedQuery(
        name="findSortiesByCriteria",
        query="SELECT s FROM Sortie s WHERE s.descriptionSortie LIKE :pattern"
)
public class Sortie implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long idSortie ;
    
    @Basic
    private String nomSortie ;
    
    @Basic
    private String descriptionSortie ;
    
    @Basic
    private URL siteWebSortie ;
    
    @Basic
    @Temporal(TemporalType.DATE)
    private Date dateSortie ;
    
    @Version
    private long versionSortie = 0 ;
    
    @ManyToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST})
    @JoinColumn(name = "idMembre")
    @ToString.Exclude
    private Membre createurSortie;
    
    @ManyToOne(cascade = {CascadeType.MERGE, CascadeType.PERSIST})
    @ToString.Exclude
    private Categorie categorie ;
    
    public Sortie(String nomSortie, String descriptionSortie, URL siteWebSortie, Date dateSortie,Membre createurSortie,Categorie categorie) {
        this(0, nomSortie, descriptionSortie, siteWebSortie, dateSortie, 0, createurSortie, categorie);
    }
    
    @PreUpdate
    public void beforeUpdate() {
    System.err.println("PreUpdate␣of␣" + this);
    }
    @PostUpdate
    public void afterUpdate() {
    System.err.println("PostUpdate␣of␣" + this);
    }

}

测试代码

@Test
public void testGetMembreAndSorties() {
    var membre = new Membre("Katon","Den","dean.keaon@jk.com",new char[] {'j', 'j'},null);
    
    dao.addMember(membre);

    var sortie1 = new Sortie("Sortie à la montagne", "Une sortie dans la montagne.", null, null, null, null);
    
    dao.addSortie(sortie1);
    
    membre.addSortie(sortie1);
    
    Membre foundMember = dao.findMember(membre.getIdMembre());
    assertEquals(1,foundMember.getSortiesMembre().size());

    dao.removeMember(foundMember.getIdMembre());
    dao.removeSortie(sortie1.getIdSortie());
}

问题原因分析

  1. 测试流程逻辑错误:测试中先分别持久化Membre和Sortie,再调用addSortie建立关联。此时两个实体均已处于持久化状态,但JPA不会自动检测集合的修改(尤其是LAZY加载的集合),关联操作仅停留在内存中,未同步到数据库。
  2. 未触发实体更新:addSortie方法虽然双向设置了关联关系,但由于Membre和Sortie已被单独保存,后续的关联修改没有触发数据库更新操作,导致Sortie表中的idMembre字段仍为空,查询时无法关联到Membre。
  3. 事务边界问题:若DAO的每个操作都是独立事务,关联操作在事务外执行,修改无法被持久化上下文捕获并同步到数据库。

解决方案

方案一:先关联再持久化(推荐)

利用Membre上配置的Cascade.PERSIST级联操作,先建立Membre与Sortie的关联,再保存Membre,自动级联保存Sortie并同步关联关系:

@Test
public void testGetMembreAndSorties() {
    var membre = new Membre("Katon","Den","dean.keaon@jk.com",new char[] {'j', 'j'},null);
    var sortie1 = new Sortie("Sortie à la montagne", "Une sortie dans la montagne.", null, null, null, null);
    
    // 先建立双向关联
    membre.addSortie(sortie1);
    // 保存Membre,级联保存Sortie
    dao.addMember(membre);
    
    Membre foundMember = dao.findMember(membre.getIdMembre());
    assertEquals(1,foundMember.getSortiesMembre().size());

    // 利用Cascade.REMOVE级联删除Sortie,无需单独调用removeSortie
    dao.removeMember(foundMember.getIdMembre());
}

方案二:关联后手动同步修改

若必须分开保存实体,需在关联后手动触发实体更新,将内存中的关联关系同步到数据库:

@Test
public void testGetMembreAndSorties() {
    var membre = new Membre("Katon","Den","dean.keaon@jk.com",new char[] {'j', 'j'},null);
    dao.addMember(membre);

    var sortie1 = new Sortie("Sortie à la montagne", "Une sortie dans la montagne.", null, null, null, null);
    dao.addSortie(sortie1);
    
    membre.addSortie(sortie1);
    // 同步Membre的修改到数据库
    dao.updateMember(membre);
    // 或同步Sortie的关联修改
    // dao.updateSortie(sortie1);
    
    Membre foundMember = dao.findMember(membre.getIdMembre());
    assertEquals(1,foundMember.getSortiesMembre().size());

    dao.removeMember(foundMember.getIdMembre());
    dao.removeSortie(sortie1.getIdSortie());
}

额外注意事项

  • 确保DAO的updateMember/updateSortie方法内部调用了entityManager.merge()或entityManager.flush(),将修改同步到数据库。
  • 若测试方法未开启事务,查询foundMember时需注意LAZY加载集合的问题(可通过JOIN FETCH查询主动加载关联集合,避免懒加载异常)。

内容的提问来源于stack exchange,提问作者Big H

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 14:20:00