From 56b22aeaac2ca10ff4ab476c21a63e2d0e991412 Mon Sep 17 00:00:00 2001 From: Gail Badner Date: Fri, 22 Jul 2016 14:14:16 -0700 Subject: [PATCH] HHH-10795 : StatefulPersistenceContext.entityEntryContext does not work properly for ManagedEntity associated with a different StatefulPersistenceContext --- .../engine/internal/EntityEntryContext.java | 318 +++++++++++++----- .../internal/StatefulPersistenceContext.java | 2 +- .../hibernate/internal/CoreMessageLogger.java | 4 + .../bytecode/enhancement/EnhancerTest.java | 6 + .../OtherEntityEntryContextTestTask.java | 100 ++++++ .../otherentityentrycontext/Parent.java | 37 ++ ...deEnhancedImmutableReferenceCacheTest.java | 121 +++++++ 7 files changed, 504 insertions(+), 84 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/otherentityentrycontext/OtherEntityEntryContextTestTask.java create mode 100644 hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/otherentityentrycontext/Parent.java diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java index a6d9027587..97f20b1835 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/EntityEntryContext.java @@ -6,12 +6,14 @@ */ package org.hibernate.engine.internal; +import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.ManagedEntity; import org.hibernate.engine.spi.PersistenceContext; -import org.jboss.logging.Logger; +import org.hibernate.internal.CoreLogging; +import org.hibernate.internal.CoreMessageLogger; import java.io.IOException; import java.io.ObjectInputStream; @@ -37,7 +39,11 @@ import java.util.Map; * @author Steve Ebersole */ public class EntityEntryContext { - private static final Logger log = Logger.getLogger( EntityEntryContext.class ); + private static final CoreMessageLogger log = CoreLogging.messageLogger( EntityEntryContext.class ); + + private transient PersistenceContext persistenceContext; + + private transient IdentityHashMap immutableManagedEntityXref; private transient ManagedEntity head; private transient ManagedEntity tail; @@ -52,7 +58,8 @@ public class EntityEntryContext { /** * Constructs a EntityEntryContext */ - public EntityEntryContext() { + public EntityEntryContext(PersistenceContext persistenceContext) { + this.persistenceContext = persistenceContext; } /** @@ -69,33 +76,47 @@ public class EntityEntryContext { // any addition (even the double one described above) should invalidate the cross-ref array dirty = true; - // determine the appropriate ManagedEntity instance to use based on whether the entity is enhanced or not. - // also track whether the entity was already associated with the context - final ManagedEntity managedEntity; - final boolean alreadyAssociated; - if ( ManagedEntity.class.isInstance( entity ) ) { - managedEntity = (ManagedEntity) entity; - alreadyAssociated = managedEntity.$$_hibernate_getEntityEntry() != null; + assert AbstractEntityEntry.class.isInstance( entityEntry ); + + // We only need to check a mutable EntityEntry is associated with the same PersistenceContext. + // Immutable EntityEntry can be associated with multiple PersistenceContexts, so no need to check. + // ImmutableEntityEntry#getPersistenceContext() throws an exception (HHH-10251). + if ( entityEntry.getPersister().isMutable() ) { + assert AbstractEntityEntry.class.cast( entityEntry ).getPersistenceContext() == persistenceContext; } - else { - ManagedEntity wrapper = null; - if ( nonEnhancedEntityXref == null ) { - nonEnhancedEntityXref = new IdentityHashMap(); + + // Determine the appropriate ManagedEntity instance to use based on whether the entity is enhanced or not. + // Throw an exception if entity is a mutable ManagedEntity that is associated with a different + // PersistenceContext. + ManagedEntity managedEntity = getAssociatedManagedEntity( entity ); + final boolean alreadyAssociated = managedEntity != null; + if ( !alreadyAssociated ) { + if ( ManagedEntity.class.isInstance( entity ) ) { + if ( entityEntry.getPersister().isMutable() ) { + managedEntity = (ManagedEntity) entity; + // We know that managedEntity is not associated with the same PersistenceContext. + // Check if managedEntity is associated with a different PersistenceContext. + checkNotAssociatedWithOtherPersistenceContextIfMutable( managedEntity ); + } + else { + // Create a holder for PersistenceContext-related data. + managedEntity = new ImmutableManagedEntityHolder( (ManagedEntity) entity ); + if ( immutableManagedEntityXref == null ) { + immutableManagedEntityXref = new IdentityHashMap(); + } + immutableManagedEntityXref.put( + (ManagedEntity) entity, + (ImmutableManagedEntityHolder) managedEntity + ); + } } else { - wrapper = nonEnhancedEntityXref.get( entity ); + if ( nonEnhancedEntityXref == null ) { + nonEnhancedEntityXref = new IdentityHashMap(); + } + managedEntity = new ManagedEntityImpl( entity ); + nonEnhancedEntityXref.put( entity, managedEntity ); } - - if ( wrapper == null ) { - wrapper = new ManagedEntityImpl( entity ); - nonEnhancedEntityXref.put( entity, wrapper ); - alreadyAssociated = false; - } - else { - alreadyAssociated = true; - } - - managedEntity = wrapper; } // associate the EntityEntry with the entity @@ -106,9 +127,14 @@ public class EntityEntryContext { return; } + // TODO: can dirty be set to true here? + // finally, set up linking and count if ( tail == null ) { assert head == null; + // Protect against stale data in the ManagedEntity and nullify previous/next references. + managedEntity.$$_hibernate_setPreviousManagedEntity( null ); + managedEntity.$$_hibernate_setNextManagedEntity( null ); head = managedEntity; tail = head; count = 1; @@ -116,11 +142,65 @@ public class EntityEntryContext { else { tail.$$_hibernate_setNextManagedEntity( managedEntity ); managedEntity.$$_hibernate_setPreviousManagedEntity( tail ); + // Protect against stale data left in the ManagedEntity nullify next reference. + managedEntity.$$_hibernate_setNextManagedEntity( null ); tail = managedEntity; count++; } } + private ManagedEntity getAssociatedManagedEntity(Object entity) { + if ( ManagedEntity.class.isInstance( entity ) ) { + final ManagedEntity managedEntity = (ManagedEntity) entity; + if ( managedEntity.$$_hibernate_getEntityEntry() == null ) { + // it is not associated + return null; + } + final AbstractEntityEntry entityEntry = (AbstractEntityEntry) managedEntity.$$_hibernate_getEntityEntry(); + + if ( entityEntry.getPersister().isMutable() ) { + return entityEntry.getPersistenceContext() == persistenceContext + ? managedEntity // it is associated + : null; + } + else { + // if managedEntity is associated with this EntityEntryContext, then + // it will have an entry in immutableManagedEntityXref and its + // holder will be returned. + return immutableManagedEntityXref != null + ? immutableManagedEntityXref.get( managedEntity ) + : null; + } + } + else { + return nonEnhancedEntityXref != null + ? nonEnhancedEntityXref.get( entity ) + : null; + } + } + + private void checkNotAssociatedWithOtherPersistenceContextIfMutable(ManagedEntity managedEntity) { + // we only have to check mutable managedEntity + final AbstractEntityEntry entityEntry = (AbstractEntityEntry) managedEntity.$$_hibernate_getEntityEntry(); + if ( entityEntry == null || + !entityEntry.getPersister().isMutable() || + entityEntry.getPersistenceContext() == null || + entityEntry.getPersistenceContext() == persistenceContext ) { + return; + } + if ( entityEntry.getPersistenceContext().getSession().isOpen() ) { + // NOTE: otherPersistenceContext may be operating on the entityEntry in a different thread. + // it is not safe to associate entityEntry with this EntityEntryContext. + throw new HibernateException( + "Illegal attempt to associate a ManagedEntity with two open persistence contexts. " + entityEntry + ); + } + else { + // otherPersistenceContext is associated with a closed PersistenceContext + log.stalePersistenceContextInEntityEntry( entityEntry.toString() ); + } + } + /** * Does this entity exist in this context, associated with an EntityEntry? * @@ -140,17 +220,8 @@ public class EntityEntryContext { * @return The associated EntityEntry */ public EntityEntry getEntityEntry(Object entity) { - // essentially resolve the entity to a ManagedEntity... - final ManagedEntity managedEntity; - if ( ManagedEntity.class.isInstance( entity ) ) { - managedEntity = (ManagedEntity) entity; - } - else if ( nonEnhancedEntityXref == null ) { - managedEntity = null; - } - else { - managedEntity = nonEnhancedEntityXref.get( entity ); - } + // locate a ManagedEntity for the entity, but only if it is associated with the same PersistenceContext. + final ManagedEntity managedEntity = getAssociatedManagedEntity( entity ); // and get/return the EntityEntry from the ManagedEntry return managedEntity == null @@ -166,27 +237,24 @@ public class EntityEntryContext { * @return Tjee EntityEntry */ public EntityEntry removeEntityEntry(Object entity) { - dirty = true; - - // again, resolve the entity to a ManagedEntity (which may not be possible for non-enhanced)... - final ManagedEntity managedEntity; - if ( ManagedEntity.class.isInstance( entity ) ) { - managedEntity = (ManagedEntity) entity; - } - else if ( nonEnhancedEntityXref == null ) { - managedEntity = null; - } - else { - managedEntity = nonEnhancedEntityXref.remove( entity ); - } - - // if we could not resolve it, just return (it was not associated with this context) - if ( managedEntity == null || managedEntity.$$_hibernate_getEntityEntry() == null) { + // locate a ManagedEntity for the entity, but only if it is associated with the same PersistenceContext. + // no need to check if the entity is a ManagedEntity that is associated with a different PersistenceContext + final ManagedEntity managedEntity = getAssociatedManagedEntity( entity ); + if ( managedEntity == null ) { + // not associated with this EntityEntryContext, so nothing to do. return null; } - // TODO: should dirty be set to true only if managedEntity is associtated with this context - // (instead of setting it at the top of this method)? + dirty = true; + + if ( ImmutableManagedEntityHolder.class.isInstance( managedEntity ) ) { + assert entity == ( (ImmutableManagedEntityHolder) managedEntity ).managedEntity; + immutableManagedEntityXref.remove( (ManagedEntity) entity ); + + } + else if ( !ManagedEntity.class.isInstance( entity ) ) { + nonEnhancedEntityXref.remove( entity ); + } // prepare for re-linking... final ManagedEntity previous = managedEntity.$$_hibernate_getPreviousManagedEntity(); @@ -228,10 +296,7 @@ public class EntityEntryContext { // finally clean out the ManagedEntity and return the associated EntityEntry final EntityEntry theEntityEntry = managedEntity.$$_hibernate_getEntityEntry(); - // need to think about implications for memory leaks here if we don't removed reference to EntityEntry - if( canClearEntityEntryReference(managedEntity) ){ - managedEntity.$$_hibernate_setEntityEntry( null ); - } + managedEntity.$$_hibernate_setEntityEntry( null ); return theEntityEntry; } @@ -270,9 +335,7 @@ public class EntityEntryContext { while ( node != null ) { final ManagedEntity nextNode = node.$$_hibernate_getNextManagedEntity(); - if( canClearEntityEntryReference(node) ){ - node.$$_hibernate_setEntityEntry( null ); - } + node.$$_hibernate_setEntityEntry( null ); node.$$_hibernate_setPreviousManagedEntity( null ); node.$$_hibernate_setNextManagedEntity( null ); @@ -280,6 +343,10 @@ public class EntityEntryContext { node = nextNode; } + if ( immutableManagedEntityXref != null ) { + immutableManagedEntityXref.clear(); + } + if ( nonEnhancedEntityXref != null ) { nonEnhancedEntityXref.clear(); } @@ -351,7 +418,7 @@ public class EntityEntryContext { final int count = ois.readInt(); log.tracef( "Starting deserialization of [%s] EntityEntry entries", count ); - final EntityEntryContext context = new EntityEntryContext(); + final EntityEntryContext context = new EntityEntryContext( rtn ); context.count = count; context.dirty = true; @@ -376,7 +443,21 @@ public class EntityEntryContext { final ManagedEntity managedEntity; if ( isEnhanced ) { - managedEntity = (ManagedEntity) entity; + if ( entry.getPersister().isMutable() ) { + managedEntity = (ManagedEntity) entity; + } + else { + managedEntity = new ImmutableManagedEntityHolder( (ManagedEntity) entity ); + if ( context.immutableManagedEntityXref == null ) { + context.immutableManagedEntityXref = + new IdentityHashMap(); + } + context.immutableManagedEntityXref.put( + (ManagedEntity) entity, + (ImmutableManagedEntityHolder) managedEntity + + ); + } } else { managedEntity = new ManagedEntityImpl( entity ); @@ -431,25 +512,6 @@ public class EntityEntryContext { return count; } - /* - Check instance type of EntityEntry and if type is ImmutableEntityEntry, check to see if entity is referenced cached in the second level cache - */ - private boolean canClearEntityEntryReference(ManagedEntity managedEntity){ - - if( managedEntity.$$_hibernate_getEntityEntry() == null ) { - return true; - } - - if( !(managedEntity.$$_hibernate_getEntityEntry() instanceof ImmutableEntityEntry) ) { - return true; - } - else if( managedEntity.$$_hibernate_getEntityEntry().getPersister().canUseReferenceCacheEntries() ) { - return false; - } - - return true; - - } /** * The wrapper for entity classes which do not implement ManagedEntity */ @@ -499,6 +561,96 @@ public class EntityEntryContext { } } + private static class ImmutableManagedEntityHolder implements ManagedEntity { + private ManagedEntity managedEntity; + private ManagedEntity previous; + private ManagedEntity next; + + public ImmutableManagedEntityHolder(ManagedEntity immutableManagedEntity) { + this.managedEntity = immutableManagedEntity; + } + + @Override + public Object $$_hibernate_getEntityInstance() { + return managedEntity.$$_hibernate_getEntityInstance(); + } + + @Override + public EntityEntry $$_hibernate_getEntityEntry() { + return managedEntity.$$_hibernate_getEntityEntry(); + } + + @Override + public void $$_hibernate_setEntityEntry(EntityEntry entityEntry) { + // need to think about implications for memory leaks here if we don't removed reference to EntityEntry + if ( entityEntry == null ) { + if ( canClearEntityEntryReference() ) { + managedEntity.$$_hibernate_setEntityEntry( null ); + } + // otherwise, do nothing. + } + else { + // TODO: we may want to do something different here if + // managedEntity is in the process of being deleted. + // An immutable ManagedEntity can be associated with + // multiple PersistenceContexts. Changing the status + // to DELETED probably should not happen directly + // in the ManagedEntity because that would affect all + // PersistenceContexts to which the ManagedEntity is + // associated. + managedEntity.$$_hibernate_setEntityEntry( entityEntry ); + } + } + + @Override + public ManagedEntity $$_hibernate_getPreviousManagedEntity() { + // previous reference cannot be stored in an immutable ManagedEntity; + // previous reference is maintained by this ManagedEntityHolder. + return previous; + } + + @Override + public void $$_hibernate_setPreviousManagedEntity(ManagedEntity previous) { + // previous reference cannot be stored in an immutable ManagedEntity; + // previous reference is maintained by this ManagedEntityHolder. + this.previous = previous; + } + + @Override + public ManagedEntity $$_hibernate_getNextManagedEntity() { + // next reference cannot be stored in an immutable ManagedEntity; + // next reference is maintained by this ManagedEntityHolder. + return next; + } + + @Override + public void $$_hibernate_setNextManagedEntity(ManagedEntity next) { + // next reference cannot be stored in an immutable ManagedEntity; + // next reference is maintained by this ManagedEntityHolder. + this.next = next; + } + + /* + Check instance type of EntityEntry and if type is ImmutableEntityEntry, check to see if entity is referenced cached in the second level cache + */ + private boolean canClearEntityEntryReference(){ + + if( managedEntity.$$_hibernate_getEntityEntry() == null ) { + return true; + } + + if( !(managedEntity.$$_hibernate_getEntityEntry() instanceof ImmutableEntityEntry) ) { + return true; + } + else if( managedEntity.$$_hibernate_getEntityEntry().getPersister().canUseReferenceCacheEntries() ) { + return false; + } + + return true; + + } + } + /** * Used in building the {@link #reentrantSafeEntityEntries()} entries */ 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 4a87d4a2a6..8fcf2a9468 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 @@ -162,7 +162,7 @@ public class StatefulPersistenceContext implements PersistenceContext { ); entitySnapshotsByKey = new HashMap<>( INIT_COLL_SIZE ); - entityEntryContext = new EntityEntryContext(); + entityEntryContext = new EntityEntryContext( this ); // entityEntries = IdentityMap.instantiateSequenced( INIT_COLL_SIZE ); collectionEntries = IdentityMap.instantiateSequenced( INIT_COLL_SIZE ); parentsByChild = new IdentityHashMap<>( INIT_COLL_SIZE ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/CoreMessageLogger.java b/hibernate-core/src/main/java/org/hibernate/internal/CoreMessageLogger.java index 0c480240c2..35375e5f17 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/CoreMessageLogger.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/CoreMessageLogger.java @@ -1751,4 +1751,8 @@ public interface CoreMessageLogger extends BasicLogger { id = 479 ) String collectionNotProcessedByFlush(String role); + + @LogMessage(level = WARN) + @Message(value = "A ManagedEntity was associated with a stale PersistenceContext. A ManagedEntity may only be associated with one PersistenceContext at a time; %s", id = 480) + void stalePersistenceContextInEntityEntry(String msg); } diff --git a/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/EnhancerTest.java b/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/EnhancerTest.java index fda7935a8e..0a34d5d467 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/EnhancerTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/EnhancerTest.java @@ -8,6 +8,7 @@ package org.hibernate.test.bytecode.enhancement; import javassist.CtClass; +import org.hibernate.test.bytecode.enhancement.otherentityentrycontext.OtherEntityEntryContextTestTask; import org.hibernate.testing.FailureExpected; import org.hibernate.testing.TestForIssue; import org.hibernate.testing.bytecode.enhancement.EnhancerTestContext; @@ -81,6 +82,11 @@ public class EnhancerTest extends BaseUnitTestCase { EnhancerTestUtils.runEnhancerTestTask( EvictionTestTask.class ); } + @Test + public void testOtherPersistenceContext() { + EnhancerTestUtils.runEnhancerTestTask( OtherEntityEntryContextTestTask.class ); + } + @Test public void testAssociation() { EnhancerTestUtils.runEnhancerTestTask( OneToOneAssociationTestTask.class ); diff --git a/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/otherentityentrycontext/OtherEntityEntryContextTestTask.java b/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/otherentityentrycontext/OtherEntityEntryContextTestTask.java new file mode 100644 index 0000000000..762627b72f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/otherentityentrycontext/OtherEntityEntryContextTestTask.java @@ -0,0 +1,100 @@ +/* + * 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 . + */ +package org.hibernate.test.bytecode.enhancement.otherentityentrycontext; + +import org.hibernate.HibernateException; +import org.hibernate.Session; +import org.hibernate.cfg.Configuration; +import org.hibernate.cfg.Environment; +import org.hibernate.engine.spi.ManagedEntity; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.test.bytecode.enhancement.AbstractEnhancerTestTask; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * This task tests ManagedEntity objects that are already associated with a different PersistenceContext. + * + * @author Gail Badner + */ +public class OtherEntityEntryContextTestTask extends AbstractEnhancerTestTask { + + public Class[] getAnnotatedClasses() { + return new Class[] {Parent.class}; + } + + public void prepare() { + Configuration cfg = new Configuration(); + cfg.setProperty( Environment.ENABLE_LAZY_LOAD_NO_TRANS, "true" ); + cfg.setProperty( Environment.USE_SECOND_LEVEL_CACHE, "false" ); + super.prepare( cfg ); + + // Create a Parent + Session s = getFactory().openSession(); + s.beginTransaction(); + s.persist( new Parent( 1, "first" ) ); + s.getTransaction().commit(); + s.close(); + } + + public void execute() { + Session s1 = getFactory().openSession(); + s1.beginTransaction(); + Parent p = (Parent) s1.get( Parent.class, 1 ); + assertTrue( ManagedEntity.class.isInstance( p ) ); + p.setName( "second" ); + + assertTrue( s1.contains( p ) ); + + // open another session and evict p from the new session + Session s2 = getFactory().openSession(); + s2.beginTransaction(); + + // s2 should contains no entities + assertFalse( s2.contains( p ) ); + + // evict should do nothing, since p is not associated with s2 + s2.evict( p ); + + assertFalse( s2.contains( p ) ); + + assertNull( ( (SharedSessionContractImplementor) s2 ).getPersistenceContext().getEntry( p ) ); + + try { + s2.update( p ); + fail( "should have failed because p is already associated with a PersistenceContext that is still open." ); + } + catch (HibernateException expected) { + // expected + s2.getTransaction().rollback(); + s2.close(); + } + + s1.getTransaction().commit(); + s1.close(); + + p.setName( "third" ); + + s1 = getFactory().openSession(); + s1.getTransaction().begin(); + s1.update( p ); + assertTrue( s1.contains( p ) ); + s1.evict( p ); + assertFalse( s1.contains( p ) ); + p = s1.get( Parent.class, p.getId() ); + assertEquals( "second", p.getName() ); + s1.getTransaction().commit(); + s1.close(); + } + + protected void cleanup() { + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/otherentityentrycontext/Parent.java b/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/otherentityentrycontext/Parent.java new file mode 100644 index 0000000000..db99b995d1 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/bytecode/enhancement/otherentityentrycontext/Parent.java @@ -0,0 +1,37 @@ +package org.hibernate.test.bytecode.enhancement.otherentityentrycontext; + +import javax.persistence.Entity; +import javax.persistence.Id; + +/** + * Created by barreiro on 12/9/15. + */ +@Entity +public class Parent { + private Integer id; + private String name; + + public Parent() { + } + + public Parent(Integer id, String name) { + this.id = id; + this.name = name; + } + @Id + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/cache/ByteCodeEnhancedImmutableReferenceCacheTest.java b/hibernate-core/src/test/java/org/hibernate/test/cache/ByteCodeEnhancedImmutableReferenceCacheTest.java index 325af06229..2f3ff0c4ae 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/cache/ByteCodeEnhancedImmutableReferenceCacheTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/cache/ByteCodeEnhancedImmutableReferenceCacheTest.java @@ -15,7 +15,10 @@ import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.Configuration; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.ManagedEntity; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.engine.spi.Status; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.testing.TestForIssue; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; import org.junit.Test; @@ -24,8 +27,11 @@ import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Transient; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; /** @@ -91,6 +97,121 @@ public class ByteCodeEnhancedImmutableReferenceCacheTest extends BaseCoreFunctio s.close(); } + @Test + @TestForIssue( jiraKey = "HHH-10795") + public void testAssociatedWithMultiplePersistenceContexts() { + MyEnhancedReferenceData myReferenceData = new MyEnhancedReferenceData( 1, "first item", "abc" ); + MyEnhancedReferenceData myOtherReferenceData = new MyEnhancedReferenceData( 2, "second item", "def" ); + + // save a reference in one session + Session s1 = openSession(); + s1.beginTransaction(); + s1.save( myReferenceData ); + s1.save( myOtherReferenceData ); + s1.getTransaction().commit(); + s1.close(); + + assertNotNull( myReferenceData.$$_hibernate_getEntityEntry() ); + assertNotNull( myOtherReferenceData.$$_hibernate_getEntityEntry() ); + + // now associate myReferenceData with 2 sessions + s1 = openSession(); + s1.beginTransaction(); + myReferenceData = s1.get( MyEnhancedReferenceData.class, myReferenceData.getId() ); + myOtherReferenceData = s1.get( MyEnhancedReferenceData.class, myOtherReferenceData.getId() ); + assertTrue( s1.contains( myReferenceData ) ); + assertTrue( s1.contains( myOtherReferenceData ) ); + // prev/next references should be null; entityEntry should be non-null; + assertNull( myReferenceData.$$_hibernate_getPreviousManagedEntity() ); + assertNull( myReferenceData.$$_hibernate_getNextManagedEntity() ); + assertNull( myOtherReferenceData.$$_hibernate_getPreviousManagedEntity() ); + assertNull( myOtherReferenceData.$$_hibernate_getNextManagedEntity() ); + + assertSame( + myReferenceData.$$_hibernate_getEntityEntry(), + ( (SharedSessionContractImplementor) s1 ).getPersistenceContext().getEntry( myReferenceData ) + ); + assertSame( + myOtherReferenceData.$$_hibernate_getEntityEntry(), + ( (SharedSessionContractImplementor) s1 ).getPersistenceContext().getEntry( myOtherReferenceData ) + ); + + Session s2 = openSession(); + s2.beginTransaction(); + + // s2 should contains no entities + assertFalse( s2.contains( myReferenceData ) ); + assertFalse( s2.contains( myOtherReferenceData ) ); + + assertNull( ( (SharedSessionContractImplementor) s2 ).getPersistenceContext().getEntry( myReferenceData ) ); + assertNull( ( (SharedSessionContractImplementor) s2 ).getPersistenceContext().getEntry( myOtherReferenceData ) ); + + // evict should do nothing, since p is not associated with s2 + s2.evict( myReferenceData ); + s2.evict( myOtherReferenceData ); + + assertSame( myReferenceData, s2.get( MyEnhancedReferenceData.class, myReferenceData.getId() ) ); + assertSame( myOtherReferenceData, s2.get( MyEnhancedReferenceData.class, myOtherReferenceData.getId() ) ); + + assertTrue( s2.contains( myReferenceData ) ); + assertTrue( s2.contains( myOtherReferenceData ) ); + + // still associated with s1 + assertTrue( s1.contains( myReferenceData ) ); + assertTrue( s1.contains( myOtherReferenceData ) ); + + s2.evict( myReferenceData ); + s2.evict( myOtherReferenceData ); + + assertFalse( s2.contains( myReferenceData ) ); + assertFalse( s2.contains( myOtherReferenceData ) ); + + s2.getTransaction().commit(); + s2.close(); + + // still associated with s1 + assertTrue( s1.contains( myReferenceData ) ); + assertTrue( s1.contains( myOtherReferenceData ) ); + + s1.clear(); + + assertFalse( s1.contains( myReferenceData ) ); + assertFalse( s1.contains( myOtherReferenceData ) ); + + s1.close(); + + // EntityEntry should still be set + assertNotNull( myReferenceData.$$_hibernate_getEntityEntry() ); + assertNotNull( myOtherReferenceData.$$_hibernate_getEntityEntry() ); + + // load them into 2 sessions + s1 = openSession(); + s1.getTransaction().begin(); + assertSame( myReferenceData, s1.get( MyEnhancedReferenceData.class, myReferenceData.getId() ) ); + assertSame( myOtherReferenceData, s1.get( MyEnhancedReferenceData.class, myOtherReferenceData.getId() ) ); + + s2 = openSession(); + s2.getTransaction().begin(); + assertSame( myReferenceData, s2.get( MyEnhancedReferenceData.class, myReferenceData.getId() ) ); + assertSame( myOtherReferenceData, s2.get( MyEnhancedReferenceData.class, myOtherReferenceData.getId() ) ); + + // delete myReferenceData from s1 + s1.delete( myReferenceData ); + + // delete myOtherReferenceData from s2 + s2.delete( myOtherReferenceData ); + + s1.getTransaction().commit(); + s1.close(); + + assertEquals( Status.GONE, myReferenceData.$$_hibernate_getEntityEntry().getStatus() ); + + s2.getTransaction().commit(); + s2.close(); + + assertEquals( Status.GONE, myOtherReferenceData.$$_hibernate_getEntityEntry().getStatus() ); + } + @Entity(name = "MyEnhancedReferenceData") @Immutable @Cacheable