You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

关联实体共用@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生成逻辑冲突:

  1. 保存Class1时,@ManyToOne的CascadeType.ALL会触发Hibernate自动级联保存关联的Class2实例。
  2. Class2生成ID时执行HQL查询,Hibernate为保证查询结果一致性,会自动flush会话中的未提交操作。
  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.12 17:20:42