使用@OneToOne和@MapsId映射双父实体到子实体报错咨询
问题分析
异常的核心原因是:Child实体上同时为两个@OneToOne关联添加了@MapsId注解。@MapsId的作用是让当前实体的主键与关联实体的主键共享,一个实体只能指定一个关联作为主键来源。
当创建FirstParent时,Child的secondParent为null,Hibernate会遍历所有带@MapsId的关联尝试获取主键值,此时从null的secondParent取ID自然抛出异常。此外,两个关联都用@JoinColumn(name = "id")会导致Child表的id列同时作为两个父实体的外键,这在逻辑上也不成立——两个父实体的ID是独立生成的UUID,不可能完全相同。
修正方案
根据需求,我们调整映射逻辑:让Child的主键仅从其中一个父实体映射,另一个父实体使用独立的外键列,同时通过业务逻辑保证Child至少关联一个父实体。
修正后的代码
FirstParent 实体
@Entity @DynamicUpdate @Table(name = "first_parent") @Inheritance(strategy = InheritanceType.JOINED) public abstract class FirstParent { @Id private UUID id; @OneToOne(mappedBy = "firstParent", cascade = CascadeType.ALL, orphanRemoval = true) private Child child; protected FirstParent(Child child) { this.id = UUID.randomUUID(); setChild(child); } private void setChild(Child child) { this.child = child; if (child != null) { child.setFirstParent(this); // 无需手动设置Child的ID,@MapsId会自动同步 } } }
SecondParent 实体
@Entity @DynamicUpdate @Table(name = "second_parent") @Inheritance(strategy = InheritanceType.JOINED) public abstract class SecondParent { @Id private UUID id; @OneToOne(mappedBy = "secondParent", cascade = CascadeType.ALL, orphanRemoval = true) private Child child; protected SecondParent(Child child) { this.id = UUID.randomUUID(); setChild(child); } private void setChild(Child child) { this.child = child; if (child != null) { child.setSecondParent(this); // 无需手动设置Child的ID } } }
Child 实体
@Entity @DynamicUpdate @Table(name = "child") public class Child { @Id private UUID id; // 该关联作为Child主键的来源 @OneToOne(fetch = FetchType.LAZY) @MapsId @JoinColumn(name = "first_parent_id", nullable = true) private FirstParent firstParent; // 第二个父关联使用独立外键列 @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "second_parent_id", nullable = true) private SecondParent secondParent; // 空构造方法,Hibernate需要 public Child() {} // 业务校验:确保至少关联一个父实体 public boolean hasValidParent() { return firstParent != null || secondParent != null; } // 重写setter,自动同步主键(当未设置ID时) public void setFirstParent(FirstParent firstParent) { this.firstParent = firstParent; if (firstParent != null && this.id == null) { this.id = firstParent.getId(); } } public void setSecondParent(SecondParent secondParent) { this.secondParent = secondParent; if (secondParent != null && this.id == null) { this.id = secondParent.getId(); } } }
关键调整说明
- 移除
Child上第二个关联的@MapsId和重复的@JoinColumn(name = "id"),为第二个父实体设置独立的外键列second_parent_id。 - 去掉手动设置
ChildID的代码,由@MapsId自动同步第一个父实体的ID;如果Child先关联SecondParent,则在setter中手动同步ID。 - 添加
hasValidParent()方法进行业务校验,确保Child不会脱离父实体存在。 - 为
@OneToOne关联添加orphanRemoval = true,确保父实体删除时自动移除关联的Child。
内容的提问来源于stack exchange,提问作者AiR
相关产品推荐
相关产品推荐

