Spring JPA更新父对象时为何合并复制子对象并置空属性?
问题现象
将旧批处理应用从Spring 2/3升级至Spring 5(技术栈:Java 8、Spring 5.3.29、Spring Boot 2.7.16)后,执行父对象更新并添加新子对象操作时,调用repository.save()抛出非空约束错误:
not-null property references a null or transient value
调试确认:save()触发了merge流程,新添加的子对象被复制为属性全空的新对象并替换原对象,最终触发数据库非空约束。
相关实体代码
父对象DocDlvr
@Entity @Table(name="DOC_DLVR") @Inheritance(strategy=InheritanceType.SINGLE_TABLE) @DiscriminatorColumn( name="DOC_DLVR_CHNL_CD", discriminatorType=DiscriminatorType.STRING ) abstract public class DocDlvr { ... private Set<DocDlvrActy> docDlvrActy = new HashSet<DocDlvrActy>(); @OneToMany(fetch=FetchType.EAGER) @JoinColumn(name="DOC_DLVR_ID", nullable=false) @Cascade({CascadeType.PERSIST}) public Set<DocDlvrActy> getDocDlvrActy() { return docDlvrActy; } }
子对象DocDlvrActy
@Entity @Table(name = "DOC_DLVR_ACTY") public class DocDlvrActy { .... @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "DOC_DLVR_ACTY_ID", unique = true, nullable = false) public Integer getDocDlvrActyId() { return docDlvrActyId; } @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "DOC_DLVR_ID", nullable = false, insertable = false, updatable = false) public DocDlvr getDocDlvr() { return docDlvr; } public void setDocDlvr(DocDlvr docDlvr) { this.docDlvr = docDlvr; } }
业务执行逻辑
- 更新父对象状态并添加新子对象:
docDlvr.setDlvrStatCd(docDeliveryStatusEnum); docDlvr.setUpdtId(publishDocVO.getUserId()); DocDlvrActy docDlvrActy = new DocDlvrActy(); docDlvrActy.setDocDlvr(docDlvr); docDlvrActy.setActyCd(docActyTypeEnum); docDlvrActy.setCreaId(publishDocVO.getUserId()); docDlvrActy.setCreateTimeStamp(new Timestamp(new Date().getTime())); docDlvr.getDocDlvrActy().add(docDlvrActy); returnedMailBO.updateDocDlvr(docDlvr);
- 服务层执行持久化操作:
Timestamp now = new Timestamp((new java.util.Date()).getTime()); docDlvr.setUpdtTimeStamp(now); docDlvrRepository.save(docDlvr);
异常触发核心原因
级联配置不匹配:父对象
@OneToMany仅配置了CascadeType.PERSIST,而save()触发merge操作时,PERSIST级联不覆盖merge流程。merge需要显式配置CascadeType.MERGE才能将级联操作应用到新添加的子对象,否则子对象无法被正确合并到持久化上下文。子对象外键维护冲突:子对象
@JoinColumn设置了insertable=false, updatable=false,意味着JPA不会自动维护该外键字段。但父对象的@JoinColumn设置了nullable=false,同时数据库中外键字段有非空约束。手动设置子对象的父引用后,merge流程复制子对象时,由于JPA无法插入外键值,且复制后的对象丢失了原有的父引用,导致外键为空触发非空错误。游离对象触发merge:未配置显式事务时,从数据库获取的父对象在后续操作中可能脱离持久化上下文(处于游离状态)。Spring Data JPA的
save()方法对游离对象会触发merge而非persist,merge操作会复制对象状态到持久化上下文的实例中,若级联配置不足,新子对象无法被正确处理,导致复制出属性全空的对象。版本升级后的规范严格性变化:Spring 5对应的Hibernate 5.6+版本在JPA规范的执行上比旧版本更严格,旧版本中可能允许未正确配置的级联操作隐式处理,新版本则严格遵循规范,暴露了之前隐藏的配置缺陷。
内容的提问来源于stack exchange,提问作者user26270

