Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 <em>stateful</em> implementation of the {@link PersistenceContext} contract, meaning that we maintain this
Expand Down Expand Up @@ -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();
}
}
}
}
}
Expand Down Expand Up @@ -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 );
}
Expand All @@ -2255,7 +2267,8 @@ public static EntityHolderImpl forEntity(EntityKey entityKey, EntityPersister de
enum EntityHolderState {
UNINITIALIZED,
ENHANCED_PROXY,
INITIALIZED
INITIALIZED,
DETACHED
}

// NATURAL ID RESOLUTION HANDLING ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand All @@ -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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
Expand Down
Original file line number Diff line number Diff line change
@@ -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<LocalizedDescription> query = cb.createQuery( LocalizedDescription.class );
Root<LocalizedDescription> root = query.from( LocalizedDescription.class );
query.select( root );

EntityGraph<LocalizedDescription> localizedDescriptionGraph =
em.createEntityGraph( LocalizedDescription.class );
Subgraph<Object> descriptionGraph = localizedDescriptionGraph.addSubgraph( "description" );
Subgraph<Object> 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<Version> 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;
}
}

}
Loading