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()); }
问题原因分析
- 测试流程逻辑错误:测试中先分别持久化Membre和Sortie,再调用
addSortie建立关联。此时两个实体均已处于持久化状态,但JPA不会自动检测集合的修改(尤其是LAZY加载的集合),关联操作仅停留在内存中,未同步到数据库。 - 未触发实体更新:
addSortie方法虽然双向设置了关联关系,但由于Membre和Sortie已被单独保存,后续的关联修改没有触发数据库更新操作,导致Sortie表中的idMembre字段仍为空,查询时无法关联到Membre。 - 事务边界问题:若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
相关产品推荐
相关产品推荐

