双向EagerFetch的@OneToMany映射为何未出现循环获取错误?
背景
数据库中有3张表,对应Java应用中的3个JPA实体,实体代码如下:
@Data @Entity public class Fraud { @Id @Column(name = "id") private Integer id; @Column(name = "fraud_type") private String fraudType; @Column(name = "fraud_value") private String fraudValue; @OneToMany(mappedBy = "fraud", fetch = FetchType.EAGER) private List<FraudActionEntity> fraudActions; } @Data @Entity public class FraudActionEntity { @Id @Column(name = "id") private Integer id; @ManyToOne @JoinColumn(name = "fraud_id") private Fraud fraud; @ManyToOne @JoinColumn(name = "action_id") private Action action; @Column(name = "enabled") private Boolean enabled; } @Data @Entity public class Action { @Id @Column(name = "id") private Integer id; @Column(name = "attribute_key") private String attributeKey; @Column(name = "attribute_value") private String attributeValue; } @Repository public interface FraudRepository extends JpaRepository<Fraud, Integer> { public Fraud findByFraudTypeAndFraudValue(String fraudType, String fraudValue); }
业务场景为:针对特定类型的欺诈,遍历其触发的所有操作并处理。访问代码如下:
Fraud fraud = fraudRepository.findByFraudTypeAndFraudValue("Type", "Value"); log.info(fraud.getFraudActions().get(0).getAction());
执行代码时一切正常,可获取Fraud及其关联的fraudActions,未报错。原本认为Fraud与FraudActionEntity双向Eager Fetch会触发循环/无限获取错误,但实际未出现。现咨询:
- 代码正常运行的原因是什么?
- 何时会触发循环获取或无限获取循环错误?
- 若出现此类错误,能否通过将@ManyToOne改为LAZY Fetch解决,如下所示:
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "fraud_id") private Fraud fraud;
1. 代码正常运行的原因
JPA的持久化上下文(比如Hibernate)会跟踪已加载的实体实例:加载Fraud时,它会被存入上下文缓存;后续加载关联的FraudActionEntity时,虽然FraudActionEntity的fraud字段是Eager Fetch,但持久化上下文发现该Fraud实例已经在缓存中,就不会重复发起SQL查询,直接引用缓存对象。
另外,你的业务代码只是获取fraudActions第一个元素的action,根本没触发从FraudActionEntity反向访问Fraud的操作(比如调用getFraud()),就算触发了,缓存机制也会阻止重复加载,自然不会出现循环问题。
2. 触发循环/无限获取错误的场景
- 实体序列化时:比如用Jackson把实体转JSON,若没配置忽略双向关联的一方,序列化
Fraud会先序列化它的fraudActions,每个FraudActionEntity又会序列化它的fraud,循环往复最终导致栈溢出。 - 打印/遍历双向关联时:比如用
@Data自动生成的toString方法打印实体,或者手动反复调用fraud.getFraudActions().get(0).getFraud().getFraudActions()...,在脱离持久化上下文的情况下,会触发无限的对象引用遍历,引发栈溢出。 - 特殊查询场景:比如错误使用
fetch join同时强制加载双向关联双方,且JPA实现未正确处理缓存,不过这种情况在主流框架(如Hibernate)中很少见。
3. 将@ManyToOne改为LAZY Fetch能否解决问题
可以,这是处理双向关联循环问题的常用方案之一。
把FraudActionEntity中的@ManyToOne改为LAZY Fetch后,默认不会主动加载关联的Fraud实例,只有显式调用fraudAction.getFraud()时才会触发加载。这样在序列化、打印实体时,只要不主动调用这个方法,就不会触发反向关联的加载,从根源上避免循环问题。
当然也可以结合其他方案,比如用Jackson的@JsonIgnore忽略双向关联的字段,或者手动重写toString方法去掉关联字段,但LAZY Fetch是从JPA层面解决问题的核心手段之一。
内容的提问来源于stack exchange,提问作者tusharRawat

