diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java
index f22ad6c35db0..63bcc03169ca 100644
--- a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java
+++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java
@@ -78,6 +78,7 @@
import static org.hibernate.engine.internal.ManagedTypeHelper.asManagedEntity;
import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable;
import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable;
+import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer;
/**
* A stateful implementation of the {@link PersistenceContext} contract, meaning that we maintain this
@@ -231,8 +232,14 @@ public void clear() {
if ( entitiesByKey != null ) {
//Strictly avoid lambdas in this case
for ( EntityHolderImpl value : entitiesByKey.values() ) {
- if ( value != null && value.proxy != null ) {
- HibernateProxy.extractLazyInitializer( value.proxy ).unsetSession();
+ if ( value != null ) {
+ value.state = EntityHolderState.DETACHED;
+ if ( value.proxy != null ) {
+ final LazyInitializer lazyInitializer = extractLazyInitializer( value.proxy );
+ if ( lazyInitializer != null ) {
+ lazyInitializer.unsetSession();
+ }
+ }
}
}
}
@@ -2243,6 +2250,11 @@ public boolean isEventuallyInitialized() {
return state == EntityHolderState.INITIALIZED || entityInitializer != null;
}
+ @Override
+ public boolean isDetached() {
+ return state == EntityHolderState.DETACHED;
+ }
+
public static EntityHolderImpl forProxy(EntityKey entityKey, EntityPersister descriptor, Object proxy) {
return new EntityHolderImpl( entityKey, descriptor, null, proxy );
}
@@ -2255,7 +2267,8 @@ public static EntityHolderImpl forEntity(EntityKey entityKey, EntityPersister de
enum EntityHolderState {
UNINITIALIZED,
ENHANCED_PROXY,
- INITIALIZED
+ INITIALIZED,
+ DETACHED
}
// NATURAL ID RESOLUTION HANDLING ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -2271,4 +2284,13 @@ public NaturalIdResolutions getNaturalIdResolutions() {
return naturalIdResolutions;
}
+ @Override
+ public EntityHolder detachEntity(EntityKey key) {
+ final EntityHolderImpl entityHolder = removeEntityHolder( key );
+ if ( entityHolder != null ) {
+ entityHolder.state = EntityHolderState.DETACHED;
+ }
+ return entityHolder;
+ }
+
}
diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityHolder.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityHolder.java
index 8502197cfb49..ece1f1c8633b 100644
--- a/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityHolder.java
+++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityHolder.java
@@ -67,4 +67,9 @@ public interface EntityHolder {
* Whether the entity is already initialized or will be initialized through an initializer eventually.
*/
boolean isEventuallyInitialized();
+
+ /**
+ * Whether the entity is detached.
+ */
+ boolean isDetached();
}
diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/PersistenceContext.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/PersistenceContext.java
index b60436330bc5..b5198bf3a4a1 100644
--- a/hibernate-core/src/main/java/org/hibernate/engine/spi/PersistenceContext.java
+++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/PersistenceContext.java
@@ -864,4 +864,12 @@ EntityHolder claimEntityHolderIfPossible(
* @return This persistence context's natural-id helper
*/
NaturalIdResolutions getNaturalIdResolutions();
+
+ /**
+ Remove the {@link EntityHolder} and set its state to DETACHED
+ */
+ default @Nullable EntityHolder detachEntity(EntityKey key) {
+ EntityHolder entityHolder = removeEntityHolder( key );
+ return entityHolder;
+ }
}
diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java
index 9b6f9714552e..c22929a5c4d1 100644
--- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java
+++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java
@@ -59,7 +59,7 @@ public void onEvict(EvictEvent event) throws HibernateException {
.getMappingMetamodel()
.getEntityDescriptor( lazyInitializer.getEntityName() );
final EntityKey key = source.generateEntityKey( id, persister );
- final EntityHolder holder = persistenceContext.removeEntityHolder( key );
+ final EntityHolder holder = persistenceContext.detachEntity( key );
// if the entity has been evicted then its holder is null
if ( holder != null && !lazyInitializer.isUninitialized() ) {
final Object entity = holder.getEntity();
diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java
index 8c61d7a7e299..1c943c33c10c 100644
--- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java
+++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java
@@ -565,7 +565,7 @@ protected void resolveKey(EntityInitializerData data, boolean entityKeyOnly) {
}
if ( oldEntityKey != null && previousRowReuse && oldEntityInstance != null
- && areKeysEqual( oldEntityKey.getIdentifier(), id ) ) {
+ && areKeysEqual( oldEntityKey.getIdentifier(), id ) && !oldEntityHolder.isDetached() ) {
data.setState( State.INITIALIZED );
data.entityKey = oldEntityKey;
data.setInstance( oldEntityInstance );
diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/DetachedPreviousRowStateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/DetachedPreviousRowStateTest.java
new file mode 100644
index 000000000000..5f1b5ebf2ed0
--- /dev/null
+++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/DetachedPreviousRowStateTest.java
@@ -0,0 +1,177 @@
+/*
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ * Copyright Red Hat Inc. and Hibernate Authors
+ */
+package org.hibernate.orm.test.jpa;
+
+import jakarta.persistence.Entity;
+import jakarta.persistence.EntityGraph;
+import jakarta.persistence.FetchType;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.Id;
+import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.OneToOne;
+import jakarta.persistence.Subgraph;
+import jakarta.persistence.Table;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Root;
+import org.hibernate.Hibernate;
+import org.hibernate.jpa.SpecHints;
+import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
+import org.hibernate.testing.orm.junit.JiraKey;
+import org.hibernate.testing.orm.junit.Jpa;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * @author Réda Housni Alaoui
+ */
+@Jpa(annotatedClasses = {
+ DetachedPreviousRowStateTest.Version.class,
+ DetachedPreviousRowStateTest.Product.class,
+ DetachedPreviousRowStateTest.Description.class,
+ DetachedPreviousRowStateTest.LocalizedDescription.class
+})
+class DetachedPreviousRowStateTest {
+
+ @BeforeEach
+ void setupData(EntityManagerFactoryScope scope) {
+ scope.inTransaction( em -> {
+ Product product = new Product();
+ em.persist( product );
+
+ Description description = new Description( product );
+ em.persist( description );
+
+ LocalizedDescription englishDescription = new LocalizedDescription( description );
+ em.persist( englishDescription );
+ LocalizedDescription frenchDescription = new LocalizedDescription( description );
+ em.persist( frenchDescription );
+ } );
+ }
+
+ @AfterEach
+ void cleanupData(EntityManagerFactoryScope scope) {
+ scope.inTransaction( em -> {
+ em.createQuery( "delete from LocalizedDescription l" ).executeUpdate();
+ em.createQuery( "delete from Description d" ).executeUpdate();
+ em.createQuery( "delete from Product p" ).executeUpdate();
+ } );
+ }
+
+ @Test
+ @JiraKey(value = "HHH-18719")
+ void test(EntityManagerFactoryScope scope) {
+ scope.inTransaction( em -> {
+ CriteriaBuilder cb = em.getCriteriaBuilder();
+ CriteriaQuery query = cb.createQuery( LocalizedDescription.class );
+ Root root = query.from( LocalizedDescription.class );
+ query.select( root );
+
+ EntityGraph localizedDescriptionGraph =
+ em.createEntityGraph( LocalizedDescription.class );
+ Subgraph