设置ManyToOne懒加载时Hibernate出现StackOverflowError的原因排查
问题分析与解决方案
这个问题核心是Hibernate在处理嵌入式复合ID关联时的循环加载问题,结合你使用的Hibernate 4.2.6.Final版本的特性,咱们来拆解原因和解决办法:
问题根源
- 复合ID的循环依赖:你的
SimulationSearchId里包含了Simulation关联,而Simulation又持有Set<SimulationSearch>;同时SimulationSearchPropertyId里的SimulationSearch关联又会反向触发加载,加上你设置了FetchType.EAGER(强制立即加载所有关联),Hibernate会陷入递归查询的死循环,直到栈溢出。 - Hibernate 4.2.x的版本局限:这个版本对嵌入式复合ID中包含实体关联的场景处理不够完善,即使你设置了懒加载,也可能因为EAGER集合的强制加载逻辑导致懒加载失效,触发循环查询。
解决方案
方案一:重构复合ID(推荐,彻底解决问题)
把嵌入式复合ID改成普通自增ID,用独立的关联字段代替ID中的实体引用,彻底打破循环依赖:
修改Simulation.java
@Data @EqualsAndHashCode(of = "id") @ToString(exclude = "searches") @Entity @Table(name = "SIMULATION") public class Simulation implements Serializable { @Id @GeneratedValue(strategy = GenerationType.AUTO, generator = "simulation_generator") @SequenceGenerator(name = "simulation_generator", sequenceName = "SIMULATION_SEQ", allocationSize = 1) private Long id; // 改用mappedBy,让SimulationSearch维护关联关系,同时设置LAZY加载 @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "simulation") private Set<SimulationSearch> searches = new HashSet<>(0); // 其他字段 }
修改SimulationSearch.java
@Data @EqualsAndHashCode(of = "id") @ToString(exclude = "properties") @Entity @Table(name = "SIM_SEARCH") public class SimulationSearch implements Serializable { // 新增普通自增ID @Id @GeneratedValue(strategy = GenerationType.AUTO, generator = "sim_search_generator") @SequenceGenerator(name = "sim_search_generator", sequenceName = "SIM_SEARCH_SEQ", allocationSize = 1) private Long id; // 独立的Simulation关联字段 @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "SIMULATION_ID") private Simulation simulation; // position作为普通字段保留 private int position; // 改用mappedBy,让SimulationSearchProperty维护关联 @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "search") private Set<SimulationSearchProperty> properties = new HashSet<>(0); // 移除原有的嵌入式SimulationSearchId类 }
修改SimulationSearchProperty.java
@Data @EqualsAndHashCode(of = "id") @Entity @Table(name = "SIM_SEARCH_PROPERTY") public class SimulationSearchProperty implements Serializable { // 新增普通自增ID @Id @GeneratedValue(strategy = GenerationType.AUTO, generator = "sim_prop_generator") @SequenceGenerator(name = "sim_prop_generator", sequenceName = "SIM_SEARCH_PROP_SEQ", allocationSize = 1) private Long id; // 独立的SimulationSearch关联字段 @ManyToOne(fetch = FetchType.LAZY) @JoinColumns({ @JoinColumn(name = "SIMULATION_ID", referencedColumnName = "SIMULATION_ID"), @JoinColumn(name = "POSITION", referencedColumnName = "POSITION") }) private SimulationSearch search; // label作为普通字段保留 private String label; private String value; // 移除原有的嵌入式SimulationSearchPropertyId类 }
这样调整后,关联关系清晰,没有循环依赖,LAZY加载也能正常生效,加载Simulation时只会查询主表,只有当你主动访问searches或properties时才会触发关联查询。
方案二:调整现有映射(不重构ID的临时方案)
如果无法改动ID结构,可以尝试以下调整:
- 将所有集合的
FetchType改为LAZY:EAGER加载是触发循环的关键,改成LAZY后Hibernate不会强制立即加载所有关联。 - 优化嵌入式ID的关联设置:在
SimulationSearchId和SimulationSearchPropertyId的@ManyToOne上添加@LazyToOne(LazyToOneOption.NO_PROXY)(需要开启Hibernate字节码增强),确保懒加载真正生效,避免关联被意外加载。 - 保持
@ToString的排除配置:你已经在Simulation和SimulationSearch的@ToString中排除了关联集合,这能避免toString时触发懒加载导致的循环,继续保持。
另外,如果你能升级到Hibernate 4.3.x或更高版本,这个版本修复了不少复合ID关联的bug,可能也能解决问题,但升级前要做好兼容性测试。
验证步骤
修改完成后,加载单个Simulation时,观察Hibernate生成的SQL:
- 应该只会执行一次
Simulation主表的查询 - 只有当你主动调用
simulation.getSearches()或search.getProperties()时,才会触发对应的关联查询 - 不会再出现重复执行同一个SQL直到栈溢出的情况
内容的提问来源于stack exchange,提问作者noiaverbale
相关产品推荐
相关产品推荐

