HHH-17489 Fix concurrent modifications during post load processing

This commit is contained in:
Marco Belladelli 2023-12-13 11:37:20 +01:00 committed by Christian Beikov
parent 1e21da14cd
commit 7e4e478505
4 changed files with 140 additions and 79 deletions

View File

@ -407,7 +407,7 @@ public class StatefulPersistenceContext implements PersistenceContext {
}
assert holder.entityInitializer == null || holder.entityInitializer == initializer;
holder.entityInitializer = initializer;
holder.processingState = processingState;
processingState.registerLoadingEntityHolder( holder );
return holder;
}
@ -423,57 +423,59 @@ public class StatefulPersistenceContext implements PersistenceContext {
@Override
public void postLoad(JdbcValuesSourceProcessingState processingState, Consumer<EntityHolder> holderConsumer) {
if ( entitiesByKey == null ) {
final Callback callback = processingState.getExecutionContext().getCallback();
if ( processingState.getLoadingEntityHolders() != null ) {
final EventListenerGroup<PostLoadEventListener> listenerGroup = getSession().getFactory()
.getFastSessionServices()
.eventListenerGroup_POST_LOAD;
final PostLoadEvent postLoadEvent = processingState.getPostLoadEvent();
for ( final EntityHolder holder : processingState.getLoadingEntityHolders() ) {
processLoadedEntityHolder( holder, listenerGroup, postLoadEvent, callback, holderConsumer );
}
processingState.getLoadingEntityHolders().clear();
}
if ( processingState.getReloadedEntityHolders() != null ) {
for ( final EntityHolder holder : processingState.getReloadedEntityHolders() ) {
processLoadedEntityHolder( holder, null, null, callback, holderConsumer );
}
processingState.getReloadedEntityHolders().clear();
}
}
private void processLoadedEntityHolder(
EntityHolder holder,
EventListenerGroup<PostLoadEventListener> listenerGroup,
PostLoadEvent postLoadEvent,
Callback callback,
Consumer<EntityHolder> holderConsumer) {
if ( holderConsumer != null ) {
holderConsumer.accept( holder );
}
if ( holder.getEntity() == null ) {
// It's possible that we tried to load an entity and found out it doesn't exist,
// in which case we added an entry with a null proxy and entity.
// Remove that empty entry on post load to avoid unwanted side effects
entitiesByKey.remove( holder.getEntityKey() );
return;
}
final Callback callback = processingState.getExecutionContext().getCallback();
final EventListenerGroup<PostLoadEventListener> listenerGroup = getSession().getFactory()
.getFastSessionServices()
.eventListenerGroup_POST_LOAD;
final PostLoadEvent postLoadEvent = getSession().isEventSource() ? new PostLoadEvent( getSession().asEventSource() ) : null;
for ( Iterator<EntityHolderImpl> iterator = entitiesByKey.values().iterator(); iterator.hasNext(); ) {
final EntityHolderImpl holder = iterator.next();
if ( holder.processingState == processingState ) {
if ( holderConsumer != null ) {
holderConsumer.accept( holder );
}
if ( holder.entity == null ) {
// It's possible that we tried to load an entity and found out it doesn't exist,
// in which case we added an entry with a null proxy and entity.
// Remove that empty entry on post load to avoid unwanted side effects
iterator.remove();
continue;
}
if ( postLoadEvent != null ) {
postLoadEvent.reset();
postLoadEvent.setEntity( holder.entity )
.setId( holder.entityKey.getIdentifier() )
.setPersister( holder.getDescriptor() );
listenerGroup.fireEventOnEachListener(
postLoadEvent,
PostLoadEventListener::onPostLoad
);
}
if ( callback != null ) {
callback.invokeAfterLoadActions(
holder.getEntity(),
holder.getDescriptor(),
getSession()
);
if ( holder.reloaded ) {
callback.invokeAfterLoadActions(
holder.getEntity(),
holder.getDescriptor(),
getSession()
);
}
}
holder.processingState = null;
holder.entityInitializer = null;
holder.reloaded = false;
}
if ( postLoadEvent != null ) {
postLoadEvent.reset();
postLoadEvent.setEntity( holder.getEntity() )
.setId( holder.getEntityKey().getIdentifier() )
.setPersister( holder.getDescriptor() );
listenerGroup.fireEventOnEachListener(
postLoadEvent,
PostLoadEventListener::onPostLoad
);
}
if ( callback != null ) {
callback.invokeAfterLoadActions(
holder.getEntity(),
holder.getDescriptor(),
getSession()
);
}
( (EntityHolderImpl) holder ).entityInitializer = null;
}
@Override
@ -2133,8 +2135,6 @@ public class StatefulPersistenceContext implements PersistenceContext {
Object entity;
Object proxy;
EntityInitializer entityInitializer;
JdbcValuesSourceProcessingState processingState;
boolean reloaded;
EntityHolderState state;
private EntityHolderImpl(EntityKey entityKey, EntityPersister descriptor, Object entity, Object proxy) {
@ -2173,9 +2173,7 @@ public class StatefulPersistenceContext implements PersistenceContext {
@Override
public void markAsReloaded(JdbcValuesSourceProcessingState processingState) {
assert this.processingState == null;
this.reloaded = true;
this.processingState = processingState;
processingState.registerReloadedEntityHolder( this );
}
@Override

View File

@ -40,6 +40,8 @@ public class JdbcValuesSourceProcessingStateStandardImpl implements JdbcValuesSo
private final ExecutionContext executionContext;
private final JdbcValuesSourceProcessingOptions processingOptions;
private List<EntityHolder> loadingEntityHolders;
private List<EntityHolder> reloadedEntityHolders;
private Map<EntityUniqueKey, Initializer> initializerByUniquKeyMap;
private Map<CollectionKey, LoadingCollectionEntry> loadingCollectionMap;
private List<CollectionInitializer> arrayInitializers;
@ -89,6 +91,32 @@ public class JdbcValuesSourceProcessingStateStandardImpl implements JdbcValuesSo
return postLoadEvent;
}
@Override
public void registerLoadingEntityHolder(EntityHolder holder) {
if ( loadingEntityHolders == null ) {
loadingEntityHolders = new ArrayList<>();
}
loadingEntityHolders.add( holder );
}
@Override
public List<EntityHolder> getLoadingEntityHolders() {
return loadingEntityHolders;
}
@Override
public void registerReloadedEntityHolder(EntityHolder holder) {
if ( reloadedEntityHolders == null ) {
reloadedEntityHolders = new ArrayList<>();
}
reloadedEntityHolders.add( holder );
}
@Override
public List<EntityHolder> getReloadedEntityHolders() {
return reloadedEntityHolders;
}
@Override
public void registerInitializer(EntityUniqueKey entityKey, Initializer initializer) {
if ( initializerByUniquKeyMap == null ) {

View File

@ -6,7 +6,10 @@
*/
package org.hibernate.sql.results.jdbc.spi;
import java.util.List;
import org.hibernate.engine.spi.CollectionKey;
import org.hibernate.engine.spi.EntityHolder;
import org.hibernate.engine.spi.EntityUniqueKey;
import org.hibernate.engine.spi.SharedSessionContractImplementor;
import org.hibernate.event.spi.PostLoadEvent;
@ -39,13 +42,20 @@ public interface JdbcValuesSourceProcessingState {
PreLoadEvent getPreLoadEvent();
PostLoadEvent getPostLoadEvent();
void registerLoadingEntityHolder(EntityHolder holder);
List<EntityHolder> getLoadingEntityHolders();
void registerReloadedEntityHolder(EntityHolder holder);
List<EntityHolder> getReloadedEntityHolders();
void registerInitializer(
EntityUniqueKey entityKey,
Initializer initializer);
Initializer findInitializer(EntityUniqueKey entityKey);
/**
* Find a LoadingCollectionEntry locally to this context.
*

View File

@ -6,44 +6,57 @@
*/
package org.hibernate.orm.test.jpa.collection;
import java.util.HashSet;
import java.util.Set;
import java.util.List;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.orm.junit.EntityManagerFactoryScope;
import org.hibernate.testing.orm.junit.Jira;
import org.hibernate.testing.orm.junit.Jpa;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import static org.junit.Assert.assertEquals;
import jakarta.persistence.criteria.CriteriaQuery;
import jakarta.persistence.criteria.Root;
@TestForIssue(jiraKey = "HHH-6043")
@Jpa(
annotatedClasses = { Child.class, Parent.class }
)
import static org.junit.jupiter.api.Assertions.assertEquals;
@Jpa( annotatedClasses = { Child.class, Parent.class } )
public class PostLoadTest {
@BeforeAll
public void setUp(EntityManagerFactoryScope scope) {
scope.inTransaction( entityManager -> {
final Parent parent1 = new Parent();
parent1.setId( 1 );
final Child child1 = new Child();
child1.setId( 1 );
child1.setDaddy( parent1 );
entityManager.persist( parent1 );
entityManager.persist( child1 );
final Parent parent2 = new Parent();
parent2.setId( 2 );
final Child child2 = new Child();
child2.setId( 2 );
child2.setDaddy( parent2 );
entityManager.persist( parent2 );
entityManager.persist( child2 );
} );
}
@AfterAll
public void tearDown(EntityManagerFactoryScope scope) {
scope.inTransaction( entityManager -> {
entityManager.createQuery( "delete from Child" ).executeUpdate();
entityManager.createQuery( "delete from Parent" ).executeUpdate();
} );
}
/**
* Load an entity with a collection of associated entities, that uses a @PostLoad method to
* access the association.
*/
@Test
@Jira( "https://hibernate.atlassian.net/browse/HHH-6043" )
public void testAccessAssociatedSetInPostLoad(EntityManagerFactoryScope scope) {
scope.inTransaction(
entityManager -> {
Child child = new Child();
child.setId( 1 );
Parent daddy = new Parent();
daddy.setId( 1 );
child.setDaddy( daddy );
Set<Child> children = new HashSet<>();
children.add( child );
daddy.setChildren( children );
entityManager.persist( daddy );
}
);
scope.inEntityManager(
entityManager -> {
Parent daddy = entityManager.find( Parent.class, 1 );
@ -52,4 +65,16 @@ public class PostLoadTest {
);
}
@Test
@Jira( "https://hibernate.atlassian.net/browse/HHH-17489" )
public void testAccessAssociatedSetInPostLoadQuery(EntityManagerFactoryScope scope) {
scope.inEntityManager( entityManager -> {
final CriteriaQuery<Parent> cq = entityManager.getCriteriaBuilder().createQuery( Parent.class );
final Root<Parent> from = cq.from( Parent.class );
final List<Parent> parents = entityManager.createQuery( cq.select( from ) ).getResultList();
assertEquals( 2, parents.size() );
assertEquals( 1, parents.get( 0 ).getNrOfChildren() );
assertEquals( 1, parents.get( 1 ).getNrOfChildren() );
} );
}
}