Mike Becker (
https://hibernate.atlassian.net/secure/ViewProfile.jspa?accountId=6321cb8...
) *updated* an issue
Hibernate ORM (
https://hibernate.atlassian.net/browse/HHH?atlOrigin=eyJpIjoiNTVlNzI2YTkz...
) / Bug (
https://hibernate.atlassian.net/browse/HHH-15512?atlOrigin=eyJpIjoiNTVlNz...
) HHH-15512 (
https://hibernate.atlassian.net/browse/HHH-15512?atlOrigin=eyJpIjoiNTVlNz...
) Mysterious complaint about orphan removal within a parameterized test case (
https://hibernate.atlassian.net/browse/HHH-15512?atlOrigin=eyJpIjoiNTVlNz...
)
Change By: Mike Becker (
https://hibernate.atlassian.net/secure/ViewProfile.jspa?accountId=6321cb8...
)
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}
(
https://hibernate.atlassian.net/browse/HHH-15512#add-comment?atlOrigin=ey...
) Add Comment (
https://hibernate.atlassian.net/browse/HHH-15512#add-comment?atlOrigin=ey...
)
Get Jira notifications on your phone! Download the Jira Cloud app for Android (
https://play.google.com/store/apps/details?id=com.atlassian.android.jira....
) or iOS (
https://itunes.apple.com/app/apple-store/id1006972087?pt=696495&ct=Em...
) This message was sent by Atlassian Jira (v1001.0.0-SNAPSHOT#100207- sha1:4ec4822 )