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

Hibernate 6.2.6中max_fetch_depth致关联字段返回null的问题问询

Hibernate 6.2.6.Final关联字段返回null问题

实体模型结构

┌──────────────────┐       ┌───────────┐
│Step Configuration│1─────*│ FormOption│
└──────────────────┘       └───────────┘
                                 1
                                 │
                                 │
                                 1
                           ┌───────────┐       ┌────┐
                           │FormVersion│*─────1│Form│
                           └───────────┘       └────┘

注:FormVersion采用由Form ID和版本号组成的复合主键。

问题描述

我们已升级至Hibernate 6.2.6.Final,当持久化各类实体后执行查询(例如按ID查询StepConfiguration),FormOption的formVersion字段返回null,但我们预期至少返回懒加载代理(因为数据保存时该字段为非null值)。经排查发现,该字段是否为null取决于Hibernate的max_fetch_depth参数值。

核心疑问

  • 是否是实体模型的设计导致了此行为?
  • 修改max_fetch_depth参数后出现该行为是否符合预期?

我们认为这可能是Hibernate的Bug(5.x版本未出现此问题),已提交官方问题追踪;同时创建了包含上述实体及失败测试用例的项目,展示当max_fetch_depth为0和1时均存在字段被错误设为null的情况。


实体代码

StepConfiguration.java

@Entity
public class StepConfiguration {

    public StepConfiguration() { }

    @OneToMany(cascade = CascadeType.ALL, orphanRemoval = true, mappedBy = "configuration", fetch = FetchType.EAGER)
    private Set<FormOption> formOptions;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public Set<FormOption> getFormOptions() {
        return formOptions;
    }

    public void setFormOptions(Set<FormOption> formOptions) {
        this.formOptions = formOptions;
    }
}

FormOption.java

@Entity
public class FormOption {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(optional = false)
    @JoinColumn(name = "stepConfiguration_id", foreignKey = @ForeignKey(name = "FK_FormOption_StepConfiguration"))
    private StepConfiguration configuration;

    @ManyToOne(optional = false)
    @JoinColumns(foreignKey = @ForeignKey(name = "FK_FormOption_FormVersion"), value = {
        @JoinColumn(name = "form_id", updatable = false),
        @JoinColumn(name = "versionNumber", updatable = false)
    })
    private FormVersion formVersion;

    public FormOption() {
    }

    public FormOption(FormVersion formVersion) {
        this.formVersion = formVersion;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public StepConfiguration getConfiguration() {
        return configuration;
    }

    public void setConfiguration(StepConfiguration configuration) {
        this.configuration = configuration;
    }

    public FormVersion getFormVersion() {
        return formVersion;
    }

    public void setFormVersion(FormVersion formVersion) {
        this.formVersion = formVersion;
    }
}

Form.java

@Entity
public class Form {

    @OneToMany(mappedBy = "id.form")
    private List<FormVersion> versions;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    protected Long id;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public List<FormVersion> getVersions() {
        return versions;
    }

    public void setVersions(List<FormVersion> versions) {
        this.versions = versions;
    }
}

FormVersion.java

@Entity
public class FormVersion {

    @EmbeddedId
    protected FormVersionId id;

    public FormVersionId getId() {
        return id;
    }

    public void setId(FormVersionId id) {
        this.id = id;
    }

    public FormVersion() {
        id = new FormVersionId();
    }

    public FormVersion(Form form, int version) {
        this();
        this.id.setForm(form);
        this.id.setVersionNumber(version);
    }
}

FormVersionId.java

@Embeddable
public class FormVersionId implements Serializable {

    @ManyToOne
    @JoinColumn(name = "form_id")
    private Form form;

    private Integer versionNumber;

    public Form getForm() {
        return form;
    }

    public void setForm(Form form) {
        this.form = form;
    }

    public Integer getVersionNumber() {
        return versionNumber;
    }

    public void setVersionNumber(Integer versionNumber) {
        this.versionNumber = versionNumber;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (!(o instanceof FormVersionId)) {
            return false;
        }
        FormVersionId that = (FormVersionId) o;
        return form.getId().equals(that.form.getId()) && versionNumber.equals(that.versionNumber);
    }

    @Override
    public int hashCode() {
        return Objects.hash(form, versionNumber);
    }
}

测试用例

我们通过配置不同持久化单元来执行不同max_fetch_depth值的测试,抽象测试类代码如下:

public abstract class AbstractTestCase {

    private StepConfiguration stepConfiguration;

    private FormOption formOption;

    private EntityManager entityManager;

    protected abstract String getEntityManagerName();

    @Before
    public void init() throws IOException {
        Properties properties = new Properties();
        properties.load(AbstractTestCase.class.getClassLoader().getResourceAsStream("database.properties"));
        entityManager = Persistence.createEntityManagerFactory(getEntityManagerName(), properties).createEntityManager();

        entityManager.getTransaction().begin();

        Form form = new Form();
        entityManager.persist(form);

        FormVersion formVersion = new FormVersion(form, 1);

        entityManager.persist(formVersion);

        stepConfiguration = new StepConfiguration();

        formOption = new FormOption();
        formOption.setConfiguration(stepConfiguration);
        formOption.setFormVersion(formVersion);
        Set<FormOption> formOptions = new HashSet<>();
        formOptions.add(formOption);
        stepConfiguration.setFormOptions(formOptions);

        entityManager.persist(stepConfiguration);
        entityManager.getTransaction().commit();
        entityManager.clear();
    }

    @After
    public void after() {
        entityManager.getTransaction().begin();
        entityManager.createQuery("DELETE from FormOption").executeUpdate();
        entityManager.createQuery("DELETE from StepConfiguration").executeUpdate();
        entityManager.createQuery("DELETE from FormVersion").executeUpdate();
        entityManager.createQuery("DELETE from Form").executeUpdate();
        entityManager.getTransaction().commit();
        entityManager.close();
    }

    @Test
    public void formVersionLoadTest() {
        Query fetchQuery = entityManager.createQuery("SELECT s FROM StepConfiguration s WHERE s.id = :id");
        fetchQuery.setParameter("id", stepConfiguration.getId());
        StepConfiguration configuration = (StepConfiguration) fetchQuery.getSingleResult();

        assertNotNull(configuration.getFormOptions());
        Assert.assertEquals(1, configuration.getFormOptions().size());
        configuration.getFormOptions().forEach(formOption -> {
            FormVersion fv = formOption.getFormVersion();
            assertNotNull(fv);
        });
    }

    @Test
    public void formOptionsLoadTest() {
        Query fetchQuery = entityManager.createQuery("SELECT fo FROM FormOption fo");
        List<FormOption> fos = (List<FormOption>) fetchQuery.getResultList();
        Assert.assertEquals(1, fos.size());
        fos.forEach(formOption -> {
            FormVersion fv = formOption.getFormVersion();
            assertNotNull(fv);
        });
    }

    @Test
    public void singleFormOptionLoadTest() {
        Query fetchQuery = entityManager.createQuery("SELECT fo FROM FormOption fo WHERE fo.id = :id");
        fetchQuery.setParameter("id", formOption.getId());
        FormOption fo = (FormOption) fetchQuery.getSingleResult();
        FormVersion fv = fo.getFormVersion();
        assertNotNull(fv);
    }
}

测试结果

当max_fetch_depth参数为0时,formVersionLoadTest测试失败,失败点如下:

Query fetchQuery = entityManager.createQuery("SELECT s FROM StepConfiguration s WHERE s.id = :id");
fetchQuery.setParameter("id", stepConfiguration.getId());
StepConfiguration configuration = (StepConfiguration) fetchQuery.getSingleResult();

assertNotNull(configuration.getFormOptions());
Assert.assertEquals(1, configuration.getFormOptions().size());
configuration.getFormOptions().forEach(formOption -> {
    FormVersion fv = formOption.getFormVersion();
    assertNotNull(fv); // <-- 此断言在max_fetch_depth为0时失败
});

当max_fetch_depth为1时,formOptionsLoadTest和singleFormOptionLoadTest测试失败,失败点如下:

@Test
public void formOptionsLoadTest() {
    Query fetchQuery = entityManager.createQuery("SELECT fo FROM FormOption fo");
    List<FormOption> fos = (List<FormOption>) fetchQuery.getResultList();
    Assert.assertEquals(1, fos.size());
    fos.forEach(formOption -> {
        FormVersion fv = formOption.getFormVersion();
        assertNotNull(fv); // <-- 此断言在max_fetch_depth为1时失败
    });
}

@Test
public void singleFormOptionLoadTest() {
    Query fetchQuery = entityManager.createQuery("SELECT fo FROM FormOption fo WHERE fo.id = :id");
    fetchQuery.setParameter("id", formOption.getId());
    FormOption fo = (FormOption) fetchQuery.getSingleResult();
    FormVersion fv = fo.getFormVersion();
    assertNotNull(fv); // <-- 此断言在max_fetch_depth为1时失败
} 

依赖配置

pom.xml中的依赖块如下:

<dependency>
    <groupId>org.hibernate</groupId>
    <artifactId>hibernate-core</artifactId>
    <version>6.2.6.Final</version>
</dependency>

<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
    <version>2.7.1</version>
    <scope>test</scope>
</dependency>

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>1.4.8</version>
</dependency>

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.13.1</version>
    <scope>test</scope>
</dependency>

内容的提问来源于stack exchange,提问作者Philip Lowney

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 15:49:52