关联实体共用@GenericGenerator引发StackOverflowError的解决咨询
问题描述
我有两个Java实体类:
Class1 代码
@Entity @Table(catalog = "mydb", schema = "dbo", name = "[myTable1]") @Getter @Setter @Builder @ToString(onlyExplicitlyIncluded = true) @AllArgsConstructor @NoArgsConstructor public class Class1 { @Id @Column(name = "ID") @GeneratedValue(generator = "myGen") @GenericGenerator(name = "myGen", strategy = "Y.X.z.generator.MyGen") private Long id; @Column(name = "TEXT") private String text; @ManyToOne(cascade = CascadeType.ALL) @JoinColumn(name = "ID") private Class2 class2; }
Class2 代码
@Entity @Table(catalog = "mydb", schema = "dbo", name = "[myTable2]") @Getter @Setter @Builder @ToString(onlyExplicitlyIncluded = true) @AllArgsConstructor @NoArgsConstructor public class Class2 { @Id @Column(name = "ID") @GeneratedValue(generator = "myGen") @GenericGenerator(name = "myGen", strategy = "de.svg.releasenotes.generator.MyGen") private Long id; @Column(name = "TEXT") private String text; }
Class1与Class2是ManyToOne关联关系,两个类都使用同一个自定义GenericGenerator(Y.X.z.generator.MyGen),生成器代码如下:
MyGen 代码
import org.hibernate.HibernateException; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.id.IdentifierGenerator; import java.io.Serializable; import java.util.stream.Stream; public class MyGen implements IdentifierGenerator { @Override public Serializable generate( SharedSessionContractImplementor session, Object obj) throws HibernateException { String query = String.format("select %s from %s", session.getEntityPersister(obj.getClass().getName(), obj) .getIdentifierPropertyName(), obj.getClass().getSimpleName()); Stream<Long> ids = session.createQuery(query).stream(); Long max = ids.max(Long::compare).orElse(0L); return max + 1; } }
该生成器通过查询对应表的最大ID并加1生成新ID。调用POST接口时出现StackOverflowError,错误日志如下:
错误日志
2022-11-15 15:27:26.214 ERROR 276510 --- [io-13778-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.StackOverflowError] with root cause java.lang.StackOverflowError: null at org.apache.logging.slf4j.SLF4JLogger.isEnabledFor(SLF4JLogger.java:213) ~[log4j-to-slf4j-2.17.2.jar:2.17.2] at org.apache.logging.slf4j.SLF4JLogger.isEnabled(SLF4JLogger.java:121) ~[log4j-to-slf4j-2.17.2.jar:2.17.2] at org.apache.logging.log4j.spi.AbstractLogger.isEnabled(AbstractLogger.java:1513) ~[log4j-api-2.17.2.jar:2.17.2] at org.jboss.logging.Log4j2Logger.isEnabled(Log4j2Logger.java:46) ~[jboss-logging-3.4.3.Final.jar:3.4.3.Final] at org.jboss.logging.Logger.isTraceEnabled(Logger.java:98) ~[jboss-logging-3.4.3.Final.jar:3.4.3.Final] at org.jboss.logging.DelegatingBasicLogger.isTraceEnabled(DelegatingBasicLogger.java:54) ~[jboss-logging-3.4.3.Final.jar:3.4.3.Final] at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:86) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.event.internal.AbstractFlushingEventListener.cascadeOnFlush(AbstractFlushingEventListener.java:159) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.event.internal.AbstractFlushingEventListener.prepareEntityFlushes(AbstractFlushingEventListener.java:149) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:82) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.event.internal.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:50) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.internal.SessionImpl.autoFlushIfRequired(SessionImpl.java:1372) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1452) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.query.internal.AbstractProducedQuery.doList(AbstractProducedQuery.java:1649) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.query.internal.AbstractProducedQuery.list(AbstractProducedQuery.java:1617) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at Y.X.z.generator.MyGen.generate(MyGen.java:25) ~[classes/:na] at org.hibernate.event.internal.AbstractSaveEventListener.saveWithGeneratedId(AbstractSaveEventListener.java:115) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.event.internal.DefaultPersistEventListener.entityIsTransient(DefaultPersistEventListener.java:185) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:128) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:118) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.internal.SessionImpl.persistOnFlush(SessionImpl.java:801) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.engine.spi.CascadingActions$8.cascade(CascadingActions.java:341) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.engine.internal.Cascade.cascadeToOne(Cascade.java:510) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.engine.internal.Cascade.cascadeAssociation(Cascade.java:434) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.engine.internal.Cascade.cascadeProperty(Cascade.java:220) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.engine.internal.Cascade.cascade(Cascade.java:153) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.event.internal.AbstractFlushingEventListener.cascadeOnFlush(AbstractFlushingEventListener.java:159) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.event.internal.AbstractFlushingEventListener.prepareEntityFlushes(AbstractFlushingEventListener.java:149) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.event.internal.AbstractFlushingEventListener.flushEverythingToExecutions(AbstractFlushingEventListener.java:82) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.event.internal.DefaultAutoFlushEventListener.onAutoFlush(DefaultAutoFlushEventListener.java:50) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.event.service.internal.EventListenerGroupImpl.fireEventOnEachListener(EventListenerGroupImpl.java:107) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.internal.SessionImpl.autoFlushIfRequired(SessionImpl.java:1372) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.internal.SessionImpl.list(SessionImpl.java:1452) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.query.internal.AbstractProducedQuery.doList(AbstractProducedQuery.java:1649) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at org.hibernate.query.internal.AbstractProducedQuery.list(AbstractProducedQuery.java:1617) ~[hibernate-core-5.6.12.Final.jar:5.6.12.Final] at Y.X.z.generator.MyGen.generate(MyGen.java:25) ~[classes/:na] ...
请问有什么解决办法?
问题分析
出现StackOverflowError的核心原因是无限循环的级联操作与ID生成逻辑冲突:
- 保存
Class1时,@ManyToOne的CascadeType.ALL会触发Hibernate自动级联保存关联的Class2实例。 Class2生成ID时执行HQL查询,Hibernate为保证查询结果一致性,会自动flush会话中的未提交操作。flush操作再次触发Class2的级联保存,进而再次调用MyGen生成ID、执行查询、触发flush,形成无限循环直至栈溢出。
另外,Class1中@JoinColumn(name = "ID")存在逻辑错误:外键与主键使用同一字段,会导致主键被外键覆盖,加剧关联逻辑混乱。
解决办法
1. 修正关联字段的外键配置
将Class1中的@JoinColumn改为正确的外键字段,避免主键与外键冲突:
@ManyToOne(cascade = CascadeType.ALL) @JoinColumn(name = "CLASS2_ID") // 替换原有的"ID" private Class2 class2;
2. 调整级联类型
避免使用CascadeType.ALL,根据业务需求选择合适的级联策略:
- 若无需级联保存
Class2,移除cascade属性或设置为CascadeType.MERGE/CascadeType.REFRESH:
@ManyToOne(cascade = {CascadeType.MERGE, CascadeType.REFRESH}) @JoinColumn(name = "CLASS2_ID") private Class2 class2;
- 若必须级联保存,提前手动保存
Class2再关联到Class1,避免Hibernate自动触发级联:
// 先保存Class2 class2Repository.save(class2); // 关联后保存Class1 class1.setClass2(class2); class1Repository.save(class1);
3. 修改ID生成器,避免自动flush
在生成ID的查询中关闭自动flush,或改用原生SQL查询,防止触发会话刷新:
方案A:关闭HQL查询的自动flush
@Override public Serializable generate( SharedSessionContractImplementor session, Object obj) throws HibernateException { String idProperty = session.getEntityPersister(obj.getClass().getName(), obj) .getIdentifierPropertyName(); String queryStr = String.format("select %s from %s", idProperty, obj.getClass().getSimpleName()); // 设置FlushMode为MANUAL,避免自动flush Stream<Long> ids = session.createQuery(queryStr) .setFlushMode(FlushMode.MANUAL) .stream(); Long max = ids.max(Long::compare).orElse(0L); return max + 1; }
方案B:使用原生SQL查询
原生SQL查询不会触发Hibernate的自动flush逻辑:
@Override public Serializable generate( SharedSessionContractImplementor session, Object obj) throws HibernateException { String tableName = session.getEntityPersister(obj.getClass().getName(), obj) .getTableName(); String idColumn = session.getEntityPersister(obj.getClass().getName(), obj) .getIdentifierColumnNames()[0]; // 原生SQL查询最大ID String queryStr = String.format("select max(%s) from %s", idColumn, tableName); Long max = session.createNativeQuery(queryStr, Long.class) .uniqueResult(); return (max == null ? 0L : max) + 1; }
内容的提问来源于stack exchange,提问作者Abdessamad Abba
相关产品推荐
相关产品推荐

