复合主键下@OneToOne映射中@JsonIgnore失效致栈溢出问题
原因分析
Lombok @Data的循环调用
@Data注解会自动生成toString()、equals()、hashCode()方法,这些方法会递归遍历关联对象。你的代码中,Parent包含Child引用,Child的复合主键CompKey又包含Parent引用,调用这些自动生成的方法时会触发无限递归,直接导致栈溢出——@JsonIgnore只作用于Jackson序列化,无法阻止Lombok生成方法的循环调用。Jackson对嵌入式主键中注解的识别限制
即使解决了Lombok的问题,@JsonIgnore在嵌入式主键类CompKey中可能无法被Jackson正确识别。当序列化Parent时,Jackson会递归处理Child的compKey字段,此时嵌入类中的@JsonIgnore无法打断双向引用的序列化循环,依然会引发栈溢出。OneToOne默认立即加载的递归加载
JPA中@OneToOne关联默认是立即加载(FetchType.EAGER),加载Parent时会立即加载关联的Child,加载Child时又会加载其compKey中的Parent,形成无限递归的加载逻辑,进一步加剧栈溢出问题。
解决方案
方案1:修复Lombok的循环调用问题
给双向关联的字段添加Lombok的排除注解,阻止自动生成的方法递归调用关联对象:
- 在
Parent类的child字段添加@ToString.Exclude和@EqualsAndHashCode.Exclude:
@Entity @AllArgsConstructor @Table(name = "parent") @Data public class Parent { @Id @Column private String id; @OneToOne(mappedBy = "compKey.parent") @ToString.Exclude @EqualsAndHashCode.Exclude private Child child; }
- 同时在
CompKey类的parent字段添加同样的排除注解:
@Embeddable @Data @AllArgsConstructor public static class CompKey implements Serializable { @OneToOne @JoinColumn @JsonIgnore @ToString.Exclude @EqualsAndHashCode.Exclude private Parent parent; @Column private String name; }
方案2:调整JPA关联的加载策略
将@OneToOne的加载方式改为懒加载,避免立即触发递归加载:
// Parent类中 @OneToOne(mappedBy = "compKey.parent", fetch = FetchType.LAZY) @ToString.Exclude @EqualsAndHashCode.Exclude private Child child; // CompKey类中 @OneToOne(fetch = FetchType.LAZY) @JoinColumn @JsonIgnore @ToString.Exclude @EqualsAndHashCode.Exclude private Parent parent;
注意:OneToOne懒加载需要确保Hibernate的代理配置正常,可在实体类添加@Proxy(lazy = true),或在配置文件中启用相关设置。
方案3:规范复合主键的映射方式(推荐)
嵌入式主键应使用基本类型的外键字段,而非直接关联实体,再通过@MapsId关联实体与主键字段,从根源避免循环引用:
- 修改
CompKey为基本类型字段:
@Embeddable @Data @AllArgsConstructor @NoArgsConstructor public static class CompKey implements Serializable { // 用字符串类型存储外键,而非实体引用 private String parentId; private String name; }
- 修改
Child类,单独维护与Parent的关联并映射主键:
@Entity @Table(name = "child") @Data @NoArgsConstructor public class Child { @EmbeddedId private CompKey compKey; @Column private String test; // 通过@MapsId关联主键中的parentId字段 @OneToOne @MapsId("parentId") @JoinColumn(name = "parent_id") @JsonIgnore @ToString.Exclude private Parent parent; }
- 修改
Parent类的关联配置:
@Entity @AllArgsConstructor @Table(name = "parent") @Data public class Parent { @Id @Column private String id; @OneToOne(mappedBy = "parent", fetch = FetchType.LAZY) @ToString.Exclude private Child child; }
这种方式既保留了Child表复合主键包含parent_id的结构,又避免了主键中直接关联实体导致的循环问题,注解也能正常生效。
内容的提问来源于stack exchange,提问作者Something Nice

