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
相关产品推荐
相关产品推荐

