I cannot even explain what is happening here, you have to see for yourself. But you need JUnit 5 to run the test case below. The exception hibernate throws is the following:
{quote}A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance: org.hibernate.bugs.TestHHH15511$EntityHHH15511.absolutelyIrrelevant{quote}
But {{absolutelyIrrelevant}} is indeed absolutely irrelevant. This collection is never touched in the entire test case.
It looks like something bad is happening, when the {{persist()}} of the entities requires an INSERT due to generation strategy IDENTITY. If I remove that, the bug does not occur. If I keep the strategy IDENTITY and add a manual {{flush()}} after persist, the bug also does not occur.
*TestHHH15512.java*
{code:java}/* * Copyright 2014 JBoss Inc * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.hibernate.bugs;
import jakarta.persistence.*; import org.hibernate.SessionFactory; import org.hibernate.Transaction; import org.hibernate.annotations.CascadeType; import org.hibernate.annotations.*; import org.hibernate.boot.MetadataSources; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource;
import java.io.Serializable; import java.util.*; import java.util.stream.Stream;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertTrue;
/** * This test case demonstrates that Hibernate throws an irritating exception * regarding orphan removal when used in a parameterized test case. */ public class TestHHH15512 {
@Entity public static class EntityHHH15512 implements Serializable {
private static final long serialVersionUID = -6257173309941686129L;
@Id private String id;
@LazyCollection(LazyCollectionOption.TRUE) @Cascade(CascadeType.ALL) @OnDelete(action = OnDeleteAction.CASCADE) @OneToMany(orphanRemoval = true, mappedBy = "parent") private final List<EntityHHH15512OtherChild> absolutelyIrrelevant = new ArrayList<>();
@LazyCollection(LazyCollectionOption.TRUE) @Cascade(CascadeType.ALL) @OnDelete(action = OnDeleteAction.CASCADE) @OneToMany(orphanRemoval = true, mappedBy = "parent") private final List<EntityHHH15512Child> children = new ArrayList<>();
public EntityHHH15512() { }
public EntityHHH15512(String id) { this.id = id; }
public String getId() { return id; }
public void addChild(EntityHHH15512Child c) { c.parent = this; children.add(c); }
public List<EntityHHH15512Child> getChildren() { return Collections.unmodifiableList(children); } }
@Entity public static class EntityHHH15512Child implements Serializable {
private static final long serialVersionUID = -2368261096109532326L;
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) // BUG // @GeneratedValue(strategy = GenerationType.AUTO) // NO BUG private Long id;
@Basic private String name;
@Basic private String value;
@ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "fk_entity_hhh15512", nullable = false) private EntityHHH15512 parent;
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getValue() { return value; }
public void setValue(String value) { this.value = value; } }
@Entity public static class EntityHHH15512OtherChild implements Serializable {
private static final long serialVersionUID = 9215581277353861289L;
@Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false) @JoinColumn(name = "fk_entity_hhh15512", nullable = false) private EntityHHH15512 parent;
public void setId(Long id) { this.id = id; } }
@Entity public static class EntityHHH15512Log { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id;
@Basic private String name;
@Basic private String message;
public EntityHHH15512Log() { }
public EntityHHH15512Log(String name, String message) { this.name = name; this.message = message; } }
private Transaction tx;
private static SessionFactory sf;
@BeforeAll public static void buildSessionFactory() throws Exception { final var prop = new Properties(); // load the default hibernate.properties from the test case templates try (final var res = TestHHH15512.class.getClassLoader().getResourceAsStream("hibernate.properties")) { prop.load(Objects.requireNonNull(res)); } final var srb = new StandardServiceRegistryBuilder() .applySettings(prop) // not part of default hibernate.properties, not required in JUnit 4 case, because magic and so... // here we have less magic, so we need to specify it manually .applySetting("hibernate.hbm2ddl.auto", "create-drop") // this is required to make getCurrentSession() work .applySetting("hibernate.current_session_context_class", "thread");
// use the default resource path from the test case templates with our own mappings final var metadata = new MetadataSources(srb.build()) .addResource("org/hibernate/orm/test/mappings-hhh15512.xml") .buildMetadata();
sf = metadata.buildSessionFactory(); }
@BeforeEach public void openTransaction() { final var s = sf.getCurrentSession(); tx = s.beginTransaction(); }
@AfterEach public void closeTransaction() { tx.commit(); }
private static Stream<Arguments> myMethodSource() { return Stream.of( Arguments.of(1, true), Arguments.of(2, false), Arguments.of(3, true), Arguments.of(4, false) ); }
@ParameterizedTest @MethodSource("myMethodSource") public void hhh15512Test(long irrelevant, boolean log) { // given final var child = new EntityHHH15512Child(); child.setName("key"); child.setValue("val"); final var e = new EntityHHH15512(UUID.randomUUID().toString()); e.addChild(child); sf.getCurrentSession().persist(e); // sf.getCurrentSession().flush(); // hides the bug even with GenerationStrategy.IDENTITY
// when serviceMethod(e, log);
// then final var testee = sf.getCurrentSession().find(EntityHHH15512.class, e.getId()); assertThat(testee, notNullValue()); final var cv = testee.getChildren().stream().filter(c -> c.getName().equals("key")).findAny(); assertTrue(cv.isPresent()); assertThat(cv.get().getValue(), equalTo(log ? "val" : "other")); }
private void serviceMethod(EntityHHH15512 e, boolean log) { // imagine this is some service method in some real world production code // in that one case it just logs some message and in the session would obviously be obtained by sessionFactory.getCurrentSession() other case it modifies a value // but in this test case this is details are not possible - but that does not change the behavior significantly important if (log) { // but writing that log message actually IS important // because it has something to do with the findByQuery() call writeLogMessage(e.getId(), "Irrelevant"); } else { e.getChildren().stream().filter(c -> c.getName().equals("key")).findAny().ifPresent(c -> c.setValue("other")); } }
private void writeLogMessage(String id, String message) { sf.getCurrentSession().save(new EntityHHH15512Log(findByQuery(id).getId(), message)); // imagine this is used to write some log message - we remove the noise here }
private EntityHHH15512 findByQuery(String id) { return sf.getCurrentSession() .createQuery("select e from TestHHH15512$EntityHHH15512 e where e.id = :id", EntityHHH15512.class) .setParameter("id", id) .getSingleResult(); } } {code}
*mappings-hhh15512.xml*
{code:xml}<?xml version="1.0" encoding="UTF-8"?> <entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd" version="1.0"> <entity class="org.hibernate.bugs.TestHHH15512$EntityHHH15512"/> <entity class="org.hibernate.bugs.TestHHH15512$EntityHHH15512Child"/> <entity class="org.hibernate.bugs.TestHHH15512$EntityHHH15512OtherChild"/> <entity class="org.hibernate.bugs.TestHHH15512$EntityHHH15512Log"/> </entity-mappings> {code}
*hibernate.properties (default from the test case template)*
{noformat}# # Hibernate, Relational Persistence for Idiomatic Java # # License: GNU Lesser General Public License (LGPL), version 2.1 or later. # See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>. #
hibernate.dialect org.hibernate.dialect.H2Dialect hibernate.connection.driver_class org.h2.Driver hibernate.connection.url jdbc:h2:mem:db1;DB_CLOSE_DELAY=-1 hibernate.connection.username sa hibernate.connection.password
hibernate.connection.pool_size 5
hibernate.show_sql false hibernate.format_sql true
hibernate.max_fetch_depth 5
hibernate.cache.region_prefix hibernate.test hibernate.cache.region.factory_class org.hibernate.testing.cache.CachingRegionFactory
# NOTE: hibernate.jdbc.batch_versioned_data should be set to false when testing with Oracle hibernate.jdbc.batch_versioned_data true
jakarta.persistence.validation.mode=NONE hibernate.service.allow_crawling=false hibernate.session.events.log=true{noformat}
*dependencies from the pom.xml (in case anyone has problems to get the JUnit 5 test case running)*
{code:xml}<dependencies> <dependency> <groupId>org.hibernate.orm</groupId> <artifactId>hibernate-testing</artifactId> <version>6.1.3.Final</version> <scope>test</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>1.4.200</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.9.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-params</artifactId> <version>5.9.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.9.0</version> <scope>test</scope> </dependency> </dependencies>{code}
|
|