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 descriptionGraph = localizedDescriptionGraph.addSubgraph( "description" ); + Subgraph productGraph = descriptionGraph.addSubgraph( "product" ); + productGraph.addSubgraph( "versions" ); + + AtomicInteger resultCount = new AtomicInteger(); + + em.createQuery( query ) + .setHint( SpecHints.HINT_SPEC_LOAD_GRAPH, localizedDescriptionGraph ) + .getResultStream() + .forEach( localizedDescription -> { + resultCount.incrementAndGet(); + + assertThat( em.contains( localizedDescription.description.product ) ) + .withFailMessage( "'localizedDescription.description.product' is detached" ) + .isTrue(); + assertThat( Hibernate.isInitialized( localizedDescription.description.product ) ) + .withFailMessage( "'localizedDescription.description.product' is not initialized" ) + .isTrue(); + + em.flush(); + em.clear(); + } ); + + assertThat( resultCount.get() ).isEqualTo( 2 ); + } ); + } + + @Entity(name = "Version") + @Table(name = "version_tbl") + public static class Version { + + @Id + @GeneratedValue + private long id; + + private String description; + + @ManyToOne(fetch = FetchType.LAZY) + private Product product; + } + + @Entity(name = "Product") + @Table(name = "product_tbl") + public static class Product { + @Id + @GeneratedValue + private long id; + + private String description; + + @OneToMany(mappedBy = "product") + private final List versions = new ArrayList<>(); + } + + @Entity(name = "Description") + @Table(name = "description_tbl") + public static class Description { + @Id + @GeneratedValue + private long id; + + private String description; + + @OneToOne(fetch = FetchType.LAZY) + private Product product; + + public Description() { + } + + public Description(Product product) { + this.product = product; + } + } + + @Entity(name = "LocalizedDescription") + @Table(name = "localized_description_tbl") + public static class LocalizedDescription { + + @Id + @GeneratedValue + private long id; + + private String localizedDescription; + + @ManyToOne(fetch = FetchType.LAZY) + private Description description; + + public LocalizedDescription() { + } + + public LocalizedDescription(Description description) { + this.description = description; + } + } + +}