From caaee7bdab0c6988a3815acfeb89b63ac975c87b Mon Sep 17 00:00:00 2001 From: Gail Badner Date: Thu, 12 Jun 2014 20:04:44 -0700 Subject: [PATCH] HHH-9106 : Merging multiple representations of the same entity (cherry picked from commit 2daaf9a1965b70897928ea222ea8708acaad3bbd) Conflicts: hibernate-core/src/test/resources/log4j.properties --- .../internal/DefaultMergeEventListener.java | 6 +- ...a => EntityCopyAllowedLoggedObserver.java} | 119 +++-- ... EntityCopyAllowedMergeEventListener.java} | 10 +- .../internal/EntityCopyAllowedObserver.java | 61 +++ .../event/internal/MergeContextTest.java | 504 ++++++++++++++++++ .../java/org/hibernate/test/ops/Category.java | 23 + ...leEntityRepresentationsNotAllowedTest.java | 9 +- ...EntityRepresentationsOrphanDeleteTest.java | 8 + ...ergeMultipleEntityRepresentationsTest.java | 91 +++- .../src/test/resources/log4j.properties | 6 +- ...aEntityCopyAllowedMergeEventListener.java} | 13 +- .../src/test/resources/log4j.properties | 6 +- 12 files changed, 794 insertions(+), 62 deletions(-) rename hibernate-core/src/main/java/org/hibernate/event/internal/{DefaultEntityCopyObserver.java => EntityCopyAllowedLoggedObserver.java} (54%) rename hibernate-core/src/main/java/org/hibernate/event/internal/{NoEntityCopiesMergeEventListener.java => EntityCopyAllowedMergeEventListener.java} (76%) create mode 100644 hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyAllowedObserver.java create mode 100644 hibernate-core/src/test/java/org/hibernate/event/internal/MergeContextTest.java rename hibernate-entitymanager/src/main/java/org/hibernate/jpa/event/internal/core/{JpaNoEntityCopiesMergeEventListener.java => JpaEntityCopyAllowedMergeEventListener.java} (72%) diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java index fb50f3271d..a6932a7e47 100755 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java @@ -64,8 +64,8 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme return ( (MergeContext) anything ).invertMap(); } - protected EntityCopyObserver createDetachedEntityCopyObserver() { - return new DefaultEntityCopyObserver(); + protected EntityCopyObserver createEntityCopyObserver() { + return new EntityCopyNotAllowedObserver(); } /** @@ -76,7 +76,7 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme * @throws HibernateException */ public void onMerge(MergeEvent event) throws HibernateException { - final EntityCopyObserver entityCopyObserver = createDetachedEntityCopyObserver(); + final EntityCopyObserver entityCopyObserver = createEntityCopyObserver(); final MergeContext mergeContext = new MergeContext( event.getSession(), entityCopyObserver ); try { onMerge( event, mergeContext ); diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEntityCopyObserver.java b/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyAllowedLoggedObserver.java similarity index 54% rename from hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEntityCopyObserver.java rename to hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyAllowedLoggedObserver.java index 4d18bdeebf..9fb087a359 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEntityCopyObserver.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyAllowedLoggedObserver.java @@ -26,73 +26,98 @@ package org.hibernate.event.internal; import java.util.IdentityHashMap; import java.util.Map; import java.util.Set; +import java.util.TreeMap; -import org.hibernate.collection.spi.PersistentCollection; -import org.hibernate.engine.spi.EntityEntry; import org.hibernate.event.spi.EventSource; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.collections.IdentitySet; -import org.hibernate.persister.collection.CollectionPersister; -import org.hibernate.persister.entity.EntityPersister; import org.hibernate.pretty.MessageHelper; -import org.hibernate.proxy.HibernateProxy; -import org.hibernate.type.AssociationType; -import org.hibernate.type.CollectionType; -import org.hibernate.type.EntityType; -import org.hibernate.type.Type; /** + * An {@link EntityCopyObserver} implementation that allows multiple representations of + * the same persistent entity to be merged and provides logging of the entity copies that + * that are detected. + * * @author Gail Badner */ -public class DefaultEntityCopyObserver implements EntityCopyObserver { - private static final CoreMessageLogger LOG = CoreLogging.messageLogger( DefaultEntityCopyObserver.class ); +public class EntityCopyAllowedLoggedObserver extends EntityCopyAllowedObserver { + private static final CoreMessageLogger LOG = CoreLogging.messageLogger( EntityCopyAllowedLoggedObserver.class ); + + // Tracks the number of entity copies per entity name. + private Map countsByEntityName; // managedToMergeEntitiesXref is only maintained for DEBUG logging so that a "nice" message // about multiple representations can be logged at the completion of the top-level merge. - // if DEBUG logging is not enabled or no entity copies have been detected, managedToMergeEntitiesXref - // will remain null; + // If no entity copies have been detected, managedToMergeEntitiesXref will remain null; private Map> managedToMergeEntitiesXref = null; // key is the managed entity; // value is the set of representations being merged corresponding to the same managed result. + /** + * Indicates if DEBUG logging is enabled. + * + * @return true, if DEBUG logging is enabled. + */ + public static boolean isDebugLoggingEnabled() { + return LOG.isDebugEnabled(); + } + @Override public void entityCopyDetected( Object managedEntity, Object mergeEntity1, Object mergeEntity2, EventSource session) { + final String entityName = session.getEntityName( managedEntity ); LOG.trace( String.format( "More than one representation of the same persistent entity being merged for: %s", MessageHelper.infoString( - session.getEntityName( managedEntity ), + entityName, session.getIdentifier( managedEntity ) ) ) ); - - if ( LOG.isDebugEnabled() ) { - // managedToMergeEntitiesXref is only maintained for DEBUG logging - Set detachedEntitiesForManaged = null; - if ( managedToMergeEntitiesXref == null ) { - // This is the first time multiple representations have been found; - // instantiate managedToMergeEntitiesXref. - managedToMergeEntitiesXref = new IdentityHashMap>(); - } - else { - // Get any existing representations that have already been found. - detachedEntitiesForManaged = managedToMergeEntitiesXref.get( managedEntity ); - } - if ( detachedEntitiesForManaged == null ) { - // There were no existing representations; instantiate detachedEntitiesForManaged - detachedEntitiesForManaged = new IdentitySet(); - managedToMergeEntitiesXref.put( managedEntity, detachedEntitiesForManaged ); - } - // Now add the detached representation for the managed entity. - detachedEntitiesForManaged.add( mergeEntity1 ); - detachedEntitiesForManaged.add( mergeEntity2 ); + Set detachedEntitiesForManaged = null; + if ( managedToMergeEntitiesXref == null ) { + // This is the first time multiple representations have been found; + // instantiate managedToMergeEntitiesXref. + managedToMergeEntitiesXref = new IdentityHashMap>(); } + else { + // Get any existing representations that have already been found. + detachedEntitiesForManaged = managedToMergeEntitiesXref.get( managedEntity ); + } + if ( detachedEntitiesForManaged == null ) { + // There were no existing representations; instantiate detachedEntitiesForManaged + detachedEntitiesForManaged = new IdentitySet(); + managedToMergeEntitiesXref.put( managedEntity, detachedEntitiesForManaged ); + } + // Now add the detached representation for the managed entity; only count the + // entity copies that have not already been added to detachedEntitiesForManaged. + if ( detachedEntitiesForManaged.add( mergeEntity1 ) ) { + incrementEntityNameCount( entityName ); + } + if ( detachedEntitiesForManaged.add( mergeEntity2 ) ) { + incrementEntityNameCount( entityName ); + } + } + + private void incrementEntityNameCount(String entityName) { + Integer countBeforeIncrement = 0; + if ( countsByEntityName == null ) { + // Use a TreeMap so counts can be logged by entity name in alphabetic order. + countsByEntityName = new TreeMap(); + } + else { + countBeforeIncrement = countsByEntityName.get( entityName ); + if ( countBeforeIncrement == null ) { + // no entity copies for entityName detected previously. + countBeforeIncrement = 0; + } + } + countsByEntityName.put( entityName, countBeforeIncrement + 1 ); } public void clear() { @@ -100,16 +125,34 @@ public class DefaultEntityCopyObserver implements EntityCopyObserver { managedToMergeEntitiesXref.clear(); managedToMergeEntitiesXref = null; } + if ( countsByEntityName != null ) { + countsByEntityName.clear(); + countsByEntityName = null; + } } @Override public void topLevelMergeComplete(EventSource session) { - if ( !LOG.isDebugEnabled() ) { - return; + // Log the summary. + if ( countsByEntityName != null ) { + for ( Map.Entry entry : countsByEntityName.entrySet() ) { + final String entityName = entry.getKey(); + final int count = entry.getValue(); + LOG.debug( + String.format( + "%s entity copies merged: %d", + entityName, + count + ) + ); + } + } + else { + LOG.debug( "No entity copies merged." ); } - if ( managedToMergeEntitiesXref != null && !managedToMergeEntitiesXref.isEmpty() ) { + if ( managedToMergeEntitiesXref != null ) { for ( Map.Entry> entry : managedToMergeEntitiesXref.entrySet() ) { Object managedEntity = entry.getKey(); Set mergeEntities = entry.getValue(); diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/NoEntityCopiesMergeEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyAllowedMergeEventListener.java similarity index 76% rename from hibernate-core/src/main/java/org/hibernate/event/internal/NoEntityCopiesMergeEventListener.java rename to hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyAllowedMergeEventListener.java index 4459df95c3..74722a84f1 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/NoEntityCopiesMergeEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyAllowedMergeEventListener.java @@ -25,15 +25,17 @@ package org.hibernate.event.internal; /** - * A {@link org.hibernate.event.spi.MergeEventListener} that does not allow merging + * A {@link org.hibernate.event.spi.MergeEventListener} that allows merging * multiple representations of the same persistent entity. * * @author Gail Badner */ -public class NoEntityCopiesMergeEventListener extends DefaultMergeEventListener { +public class EntityCopyAllowedMergeEventListener extends DefaultMergeEventListener { @Override - protected EntityCopyObserver createDetachedEntityCopyObserver() { - return new EntityCopyNotAllowedObserver(); + protected EntityCopyObserver createEntityCopyObserver() { + return EntityCopyAllowedLoggedObserver.isDebugLoggingEnabled() ? + new EntityCopyAllowedLoggedObserver() : + new EntityCopyAllowedObserver(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyAllowedObserver.java b/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyAllowedObserver.java new file mode 100644 index 0000000000..4f12f3424d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyAllowedObserver.java @@ -0,0 +1,61 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2014, Red Hat Inc. or third-party contributors as + * indicated by the @author tags or express copyright attribution + * statements applied by the authors. All third-party contributions are + * distributed under license by Red Hat Inc. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + */ +package org.hibernate.event.internal; + +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; + +import org.hibernate.event.spi.EventSource; +import org.hibernate.internal.CoreLogging; +import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.internal.util.collections.IdentitySet; +import org.hibernate.pretty.MessageHelper; + +/** + * An {@link EntityCopyObserver} implementation that allows multiple representations of + * the same persistent entity to be merged. + * + * @author Gail Badner + */ +public class EntityCopyAllowedObserver implements EntityCopyObserver { + @Override + public void entityCopyDetected( + Object managedEntity, + Object mergeEntity1, + Object mergeEntity2, + EventSource session) { + // do nothing. + } + + public void clear() { + // do nothing. + } + + + @Override + public void topLevelMergeComplete(EventSource session) { + // do nothing. + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/event/internal/MergeContextTest.java b/hibernate-core/src/test/java/org/hibernate/event/internal/MergeContextTest.java new file mode 100644 index 0000000000..887b541ff5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/event/internal/MergeContextTest.java @@ -0,0 +1,504 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as + * indicated by the @author tags or express copyright attribution + * statements applied by the authors. All third-party contributions are + * distributed under license by Red Hat Middleware LLC. + * + * This copyrighted material is made available to anyone wishing to use, modify, + * copy, or redistribute it subject to the terms and conditions of the GNU + * Lesser General Public License, as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License + * for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this distribution; if not, write to: + * Free Software Foundation, Inc. + * 51 Franklin Street, Fifth Floor + * Boston, MA 02110-1301 USA + * + */ +package org.hibernate.event.internal; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import javax.persistence.Entity; +import javax.persistence.Id; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.hibernate.event.spi.EventSource; +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * 2011/10/20 Unit test for code added in MergeContext for performance improvement. + * + * @author Wim Ockerman @ CISCO + */ +public class MergeContextTest extends BaseCoreFunctionalTestCase { + private EventSource session = null; + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Simple.class }; + } + + @Before + public void setUp() { + session = (EventSource) openSession(); + } + + @After + public void tearDown() { + session.close(); + session = null; + } + + @Test + public void testMergeToManagedEntityFillFollowedByInvertMapping() { + MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() ); + + Object mergeEntity = new Simple( 1 ); + Object managedEntity = new Simple( 2 ); + + cache.put(mergeEntity, managedEntity); + + checkCacheConsistency( cache, 1 ); + + assertTrue( cache.containsKey( mergeEntity ) ); + assertFalse( cache.containsKey( managedEntity ) ); + assertTrue( cache.containsValue( managedEntity ) ); + + assertTrue( cache.invertMap().containsKey( managedEntity ) ); + assertFalse( cache.invertMap().containsKey( mergeEntity ) ); + assertTrue( cache.invertMap().containsValue( mergeEntity ) ); + + cache.clear(); + + checkCacheConsistency( cache, 0 ); + + assertFalse(cache.containsKey(mergeEntity)); + assertFalse(cache.invertMap().containsKey(managedEntity)); + } + + @Test + public void testMergeToManagedEntityFillFollowedByInvert() { + MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() ); + + Object mergeEntity = new Simple( 1 ); + Object managedEntity = new Simple( 2 ); + + cache.put(mergeEntity, managedEntity); + + checkCacheConsistency( cache, 1 ); + + assertTrue(cache.containsKey(mergeEntity)); + assertFalse( cache.containsKey( managedEntity ) ); + + assertTrue( cache.invertMap().containsKey( managedEntity ) ); + assertFalse( cache.invertMap().containsKey( mergeEntity ) ); + } + + @Test + public void testMergeToManagedEntityFillFollowedByInvertUsingPutAll() { + MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() ); + + Map input = new HashMap(); + Object mergeEntity1 = new Simple( 1 ); + // + Object managedEntity1 = 1; + input.put(mergeEntity1, managedEntity1); + Object mergeEntity2 = new Simple( 3 ); + Object managedEntity2 = 2; + input.put(mergeEntity2, managedEntity2); + cache.putAll(input); + + checkCacheConsistency( cache, 2 ); + + assertTrue(cache.containsKey(mergeEntity1)); + assertFalse(cache.containsKey(managedEntity1)); + assertTrue(cache.containsKey(mergeEntity2)); + assertFalse(cache.containsKey(managedEntity2)); + + assertTrue(cache.invertMap().containsKey(managedEntity1)); + assertFalse(cache.invertMap().containsKey(mergeEntity1)); + + assertTrue(cache.invertMap().containsKey(managedEntity2)); + assertFalse(cache.invertMap().containsKey(mergeEntity2)); + } + + @Test + public void testMergeToManagedEntityFillFollowedByInvertUsingPutWithSetOperatedOnArg() { + MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() ); + + Object mergeEntity = new Simple( 1 ); + Object managedEntity = new Simple( 2 ); + + cache.put(mergeEntity, managedEntity, true); + + checkCacheConsistency( cache, 1 ); + + assertTrue(cache.containsKey(mergeEntity)); + assertFalse( cache.containsKey( managedEntity ) ); + + assertTrue( cache.invertMap().containsKey( managedEntity ) ); + assertFalse( cache.invertMap().containsKey( mergeEntity ) ); + + cache.clear(); + + checkCacheConsistency( cache, 0 ); + + cache.put(mergeEntity, managedEntity, false); + assertFalse( cache.isOperatedOn( mergeEntity ) ); + + checkCacheConsistency( cache, 1 ); + + assertTrue(cache.containsKey(mergeEntity)); + assertFalse(cache.containsKey(managedEntity)); + } + + @Test + public void testMergeToManagedEntityFillFollowedByIterateEntrySet() { + MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() ); + + Object mergeEntity = new Simple( 1 ); + Object managedEntity = new Simple( 2 ); + + cache.put( mergeEntity, managedEntity, true ); + + checkCacheConsistency( cache, 1 ); + + Iterator it = cache.entrySet().iterator(); + assertTrue( it.hasNext() ); + Map.Entry entry = ( Map.Entry ) it.next(); + assertSame( mergeEntity, entry.getKey() ); + assertSame( managedEntity, entry.getValue() ); + assertFalse( it.hasNext() ); + + } + + @Test + public void testMergeToManagedEntityFillFollowedByModifyEntrySet() { + MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() ); + + Object mergeEntity = new Simple( 1 ); + Object managedEntity = new Simple( 2 ); + + cache.put( mergeEntity, managedEntity, true ); + + Iterator it = cache.entrySet().iterator(); + try { + it.remove(); + fail( "should have thrown UnsupportedOperationException" ); + } + catch ( UnsupportedOperationException ex ) { + // expected + } + + Map.Entry entry = (Map.Entry) cache.entrySet().iterator().next(); + try { + cache.entrySet().remove( entry ); + fail( "should have thrown UnsupportedOperationException" ); + } + catch ( UnsupportedOperationException ex ) { + // expected + } + + Map.Entry anotherEntry = new Map.Entry() { + private Object key = new Simple( 3 ); + private Object value = 4; + @Override + public Object getKey() { + return key; + } + + @Override + public Object getValue() { + return value; + } + + @Override + public Object setValue(Object value) { + Object oldValue = this.value; + this.value = value; + return oldValue; + } + }; + try { + cache.entrySet().add( anotherEntry ); + fail( "should have thrown UnsupportedOperationException" ); + } + catch ( UnsupportedOperationException ex ) { + // expected + } + + } + + @Test + public void testMergeToManagedEntityFillFollowedByModifyKeys() { + MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() ); + + Object mergeEntity = new Simple( 1 ); + Object managedEntity = new Simple( 2 ); + + cache.put( mergeEntity, managedEntity, true ); + + Iterator it = cache.keySet().iterator(); + try { + it.remove(); + fail( "should have thrown UnsupportedOperationException" ); + } + catch ( UnsupportedOperationException ex ) { + // expected + } + + try { + cache.keySet().remove( mergeEntity ); + fail( "should have thrown UnsupportedOperationException" ); + } + catch ( UnsupportedOperationException ex ) { + // expected + } + + Object newmanagedEntity = new Simple( 3 ); + try { + cache.keySet().add( newmanagedEntity ); + fail( "should have thrown UnsupportedOperationException" ); + } + catch ( UnsupportedOperationException ex ) { + // expected + } + } + + @Test + public void testMergeToManagedEntityFillFollowedByModifyValues() { + MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() ); + + Object mergeEntity = new Simple( 1 ); + Object managedEntity = new Simple( 2 ); + + cache.put( mergeEntity, managedEntity, true ); + + Iterator it = cache.values().iterator(); + try { + it.remove(); + fail( "should have thrown UnsupportedOperationException" ); + } + catch ( UnsupportedOperationException ex ) { + // expected + } + + try { + cache.values().remove( managedEntity ); + fail( "should have thrown UnsupportedOperationException" ); + } + catch ( UnsupportedOperationException ex ) { + // expected + } + + Object newmanagedEntity = new Simple( 3 ); + try { + cache.values().add( newmanagedEntity ); + fail( "should have thrown UnsupportedOperationException" ); + } + catch ( UnsupportedOperationException ex ) { + // expected + } + } + + @Test + public void testMergeToManagedEntityFillFollowedByModifyKeyOfEntrySetElement() { + MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() ); + + Simple mergeEntity = new Simple( 1 ); + Simple managedEntity = new Simple( 0 ); + cache.put(mergeEntity, managedEntity, true); + + Map.Entry entry = (Map.Entry) cache.entrySet().iterator().next(); + ( ( Simple ) entry.getKey() ).setValue( 2 ); + assertEquals( 2, mergeEntity.getValue() ); + + checkCacheConsistency( cache, 1 ); + + entry = (Map.Entry) cache.entrySet().iterator().next(); + assertSame( mergeEntity, entry.getKey() ); + assertSame( managedEntity, entry.getValue() ); + } + + @Test + public void testMergeToManagedEntityFillFollowedByModifyValueOfEntrySetElement() { + MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() ); + + Simple mergeEntity = new Simple( 1 ); + Simple managedEntity = new Simple( 0 ); + cache.put(mergeEntity, managedEntity, true); + + Map.Entry entry = (Map.Entry) cache.entrySet().iterator().next(); + ( ( Simple ) entry.getValue() ).setValue( 2 ); + assertEquals( 2, managedEntity.getValue() ); + + checkCacheConsistency( cache, 1 ); + + entry = (Map.Entry) cache.entrySet().iterator().next(); + assertSame( mergeEntity, entry.getKey() ); + assertSame( managedEntity, entry.getValue() ); + } + + @Test + public void testReplaceManagedEntity() { + MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() ); + + Simple mergeEntity = new Simple( 1 ); + Simple managedEntity = new Simple( 0 ); + cache.put(mergeEntity, managedEntity); + + Simple managedEntityNew = new Simple( 0 ); + try { + cache.put( mergeEntity, managedEntityNew ); + } + catch( IllegalArgumentException ex) { + // expected; cannot replace the managed entity result for a particular merge entity. + } + } + + @Test + public void testManagedEntityAssociatedWithNewAndExistingMergeEntities() { + MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() ); + + session.getTransaction().begin(); + Simple mergeEntity = new Simple( 1 ); + Simple managedEntity = new Simple( 0 ); + cache.put(mergeEntity, managedEntity); + cache.put( new Simple( 1 ), managedEntity ); + } + + @Test + public void testManagedAssociatedWith2ExistingMergeEntities() { + MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() ); + + session.getTransaction().begin(); + Simple mergeEntity1 = new Simple( 1 ); + session.persist( mergeEntity1 ); + Simple managedEntity1 = new Simple( 1 ); + cache.put( mergeEntity1, managedEntity1 ); + Simple managedEntity2 = new Simple( 2 ); + + try { + cache.put( mergeEntity1, managedEntity2 ); + fail( "should have thrown IllegalArgumentException"); + } + catch( IllegalArgumentException ex ) { + // expected; cannot change managed entity associated with a merge entity + } + finally { + session.getTransaction().rollback(); + } + } + + @Test + public void testRemoveNonExistingEntity() { + MergeContext cache = new MergeContext( session, new DoNothingEntityCopyObserver() ); + try { + cache.remove( new Simple( 1 ) ); + } + catch (UnsupportedOperationException ex) { + // expected; remove is not supported. + } + } + + private void checkCacheConsistency(MergeContext cache, int expectedSize) { + Set entrySet = cache.entrySet(); + Set cacheKeys = cache.keySet(); + Collection cacheValues = cache.values(); + Map invertedMap = cache.invertMap(); + + assertEquals( expectedSize, entrySet.size() ); + assertEquals( expectedSize, cache.size() ); + assertEquals( expectedSize, cacheKeys.size() ); + assertEquals( expectedSize, cacheValues.size() ); + assertEquals( expectedSize, invertedMap.size() ); + + for ( Object entry : cache.entrySet() ) { + Map.Entry mapEntry = ( Map.Entry ) entry; + assertSame( cache.get( mapEntry.getKey() ), mapEntry.getValue() ); + assertTrue( cacheKeys.contains( mapEntry.getKey() ) ); + assertTrue( cacheValues.contains( mapEntry.getValue() ) ); + assertSame( mapEntry.getKey(), invertedMap.get( mapEntry.getValue() ) ); + } + } + + @Entity + private static class Simple { + @Id + private int value; + + public Simple(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + + @Override + public String toString() { + return "Simple{" + + "value=" + value + + '}'; + } + } + + private class DoNothingEntityCopyObserver implements EntityCopyObserver { + + @Override + public void entityCopyDetected(Object managedEntity, Object mergeEntity1, Object mergeEntity2, EventSource session) { + + } + + @Override + public void topLevelMergeComplete(EventSource session) { + + } + + @Override + public void clear() { + + } + } + + private class ExceptionThrowingEntityCopyObserver implements EntityCopyObserver { + + @Override + public void entityCopyDetected(Object managedEntity, Object mergeEntity1, Object mergeEntity2, EventSource session) { + throw new IllegalStateException( "Entity copies not allowed." ); + } + + @Override + public void topLevelMergeComplete(EventSource session) { + } + + @Override + public void clear() { + + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/Category.java b/hibernate-core/src/test/java/org/hibernate/test/ops/Category.java index 0fcdd363d0..06d395c77e 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/ops/Category.java +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/Category.java @@ -84,4 +84,27 @@ public class Category { ", version=" + version + '}'; } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + + Category category = (Category) o; + + if ( name != null ? !name.equals( category.name ) : category.name != null ) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return name != null ? name.hashCode() : 0; + } } diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsNotAllowedTest.java b/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsNotAllowedTest.java index fbef728568..6a02ff8e40 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsNotAllowedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsNotAllowedTest.java @@ -29,7 +29,7 @@ import org.junit.Test; import org.hibernate.Session; import org.hibernate.Transaction; -import org.hibernate.event.internal.NoEntityCopiesMergeEventListener; +import org.hibernate.event.internal.EntityCopyAllowedMergeEventListener; import org.hibernate.event.service.spi.EventListenerRegistry; import org.hibernate.event.spi.EventType; import org.hibernate.testing.TestForIssue; @@ -41,7 +41,7 @@ import static org.junit.Assert.assertTrue; /** * Tests merging multiple detached representations of the same entity using - * a MergeEventListener that does not allow this. + * a the default MergeEventListener (that does not allow this). * * @author Gail Badner */ @@ -54,11 +54,6 @@ public class MergeMultipleEntityRepresentationsNotAllowedTest extends BaseCoreFu }; } - protected void afterSessionFactoryBuilt() { - EventListenerRegistry registry = sessionFactory().getServiceRegistry().getService( EventListenerRegistry.class ); - registry.setListeners( EventType.MERGE, new NoEntityCopiesMergeEventListener() ); - } - @Test public void testCascadeFromDetachedToNonDirtyRepresentations() { Item item1 = new Item(); diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsOrphanDeleteTest.java b/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsOrphanDeleteTest.java index 7aa1c4decb..2b4f602ce1 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsOrphanDeleteTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsOrphanDeleteTest.java @@ -30,6 +30,9 @@ import org.junit.Test; import org.hibernate.Hibernate; import org.hibernate.Session; import org.hibernate.Transaction; +import org.hibernate.event.internal.EntityCopyAllowedMergeEventListener; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.EventType; import org.hibernate.testing.FailureExpected; import org.hibernate.testing.TestForIssue; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; @@ -54,6 +57,11 @@ public class MergeMultipleEntityRepresentationsOrphanDeleteTest extends BaseCore }; } + protected void afterSessionFactoryBuilt() { + EventListenerRegistry registry = sessionFactory().getServiceRegistry().getService( EventListenerRegistry.class ); + registry.setListeners( EventType.MERGE, new EntityCopyAllowedMergeEventListener() ); + } + @Test @FailureExpected( jiraKey = "HHH-9240" ) public void testTopLevelUnidirOneToManyBackrefWithNewElement() { diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsTest.java b/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsTest.java index d2891a604d..c2d1993a50 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsTest.java @@ -31,6 +31,9 @@ import org.hibernate.Hibernate; import org.hibernate.Session; import org.hibernate.StaleObjectStateException; import org.hibernate.Transaction; +import org.hibernate.event.internal.EntityCopyAllowedMergeEventListener; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.EventType; import org.hibernate.testing.FailureExpected; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; @@ -54,6 +57,11 @@ public class MergeMultipleEntityRepresentationsTest extends BaseCoreFunctionalTe }; } + protected void afterSessionFactoryBuilt() { + EventListenerRegistry registry = sessionFactory().getServiceRegistry().getService( EventListenerRegistry.class ); + registry.setListeners( EventType.MERGE, new EntityCopyAllowedMergeEventListener() ); + } + @Test public void testNestedDiffBasicProperty() { Item item1 = new Item(); @@ -564,6 +572,79 @@ public class MergeMultipleEntityRepresentationsTest extends BaseCoreFunctionalTe cleanup(); } + @Test + public void testCascadeFromDetachedToGT2DirtyRepresentations() { + Item item1 = new Item(); + item1.setName( "item1" ); + Category category1 = new Category(); + category1.setName( "category1" ); + item1.setCategory( category1 ); + + Hoarder hoarder = new Hoarder(); + hoarder.setName( "joe" ); + + Session s = openSession(); + Transaction tx = session.beginTransaction(); + s.persist( item1 ); + s.persist( hoarder ); + tx.commit(); + s.close(); + + // Get another representation of the same Item from a different session. + + s = openSession(); + Item item1_1 = (Item) s.get( Item.class, item1.getId() ); + s.close(); + + // item1 and item1_1 are unmodified representations of the same persistent entity. + assertFalse( item1 == item1_1 ); + assertTrue( item1.equals( item1_1 ) ); + + // Get another representation of the same Item from a different session. + + s = openSession(); + Item item1_2 = (Item) s.get( Item.class, item1.getId() ); + s.close(); + + // item1_1 and item1_2 are unmodified representations of the same persistent entity. + assertFalse( item1 == item1_2 ); + assertTrue( item1.equals( item1_2) ); + + item1_1.setName( "item1_1" ); + item1_2.setName( "item1_2" ); + + // Update hoarder (detached) to references both representations. + item1.getCategory().setExampleItem( item1_2 ); + hoarder.getItems().add( item1 ); + hoarder.setFavoriteItem( item1_1 ); + hoarder.getFavoriteItem().getCategory(); + + s = openSession(); + tx = s.beginTransaction(); + hoarder = (Hoarder) s.merge( hoarder ); + assertEquals( 1, hoarder.getItems().size() ); + assertSame( hoarder.getFavoriteItem(), hoarder.getItems().iterator().next() ); + assertSame( hoarder.getFavoriteItem(), hoarder.getFavoriteItem().getCategory().getExampleItem() ); + assertEquals( item1.getId(), hoarder.getFavoriteItem().getId() ); + assertEquals( item1.getCategory(), hoarder.getFavoriteItem().getCategory() ); + assertEquals( item1.getName(), hoarder.getFavoriteItem().getName() ); + tx.commit(); + s.close(); + + s = openSession(); + tx = s.beginTransaction(); + hoarder = (Hoarder) s.merge( hoarder ); + assertEquals( 1, hoarder.getItems().size() ); + assertSame( hoarder.getFavoriteItem(), hoarder.getItems().iterator().next() ); + assertSame( hoarder.getFavoriteItem(), hoarder.getFavoriteItem().getCategory().getExampleItem() ); + assertEquals( item1.getId(), hoarder.getFavoriteItem().getId() ); + assertEquals( item1.getCategory(), hoarder.getFavoriteItem().getCategory() ); + tx.commit(); + s.close(); + + cleanup(); + } + @Test public void testTopLevelEntityNewerThanNested() { Item item = new Item(); @@ -1111,19 +1192,27 @@ public class MergeMultipleEntityRepresentationsTest extends BaseCoreFunctionalTe } for ( Category category : (List) s.createQuery( "from Category" ).list() ) { - if ( category.getExampleItem() != null ) { + Item exampleItem = category.getExampleItem(); + if ( exampleItem != null ) { category.setExampleItem( null ); + exampleItem.setCategory( null ); s.delete( category ); + s.delete (exampleItem ); } } for ( Item item : (List) s.createQuery( "from Item" ).list() ) { + Category category = item.getCategory(); item.setCategory( null ); + if ( category != null ) { + category.setExampleItem( null ); + } s.delete( item ); } s.createQuery( "delete from Item" ).executeUpdate(); + s.getTransaction().commit(); s.close(); } diff --git a/hibernate-core/src/test/resources/log4j.properties b/hibernate-core/src/test/resources/log4j.properties index 9946036ba2..56092e0bf8 100644 --- a/hibernate-core/src/test/resources/log4j.properties +++ b/hibernate-core/src/test/resources/log4j.properties @@ -67,5 +67,7 @@ log4j.appender.leak.layout.ConversionPattern=%d{ABSOLUTE} %5p %c{1}:%L - %m%n log4j.logger.org.hibernate.testing.PossibleLeaksLogger=warn,leak -### enable the following line to log multiple entity representations being merged for a persistent entity. -#log4j.logger.org.hibernate.event.internal.DefaultEntityCopyObserver=debug +### When entity copy merge functionality is enabled, the following will provide +### information about merged entity copies. +#log4j.logger.org.hibernate.event.internal.EntityCopyAllowedLoggedObserver=debug + diff --git a/hibernate-entitymanager/src/main/java/org/hibernate/jpa/event/internal/core/JpaNoEntityCopiesMergeEventListener.java b/hibernate-entitymanager/src/main/java/org/hibernate/jpa/event/internal/core/JpaEntityCopyAllowedMergeEventListener.java similarity index 72% rename from hibernate-entitymanager/src/main/java/org/hibernate/jpa/event/internal/core/JpaNoEntityCopiesMergeEventListener.java rename to hibernate-entitymanager/src/main/java/org/hibernate/jpa/event/internal/core/JpaEntityCopyAllowedMergeEventListener.java index 50885c03a3..cba9b1c6eb 100644 --- a/hibernate-entitymanager/src/main/java/org/hibernate/jpa/event/internal/core/JpaNoEntityCopiesMergeEventListener.java +++ b/hibernate-entitymanager/src/main/java/org/hibernate/jpa/event/internal/core/JpaEntityCopyAllowedMergeEventListener.java @@ -23,20 +23,23 @@ */ package org.hibernate.jpa.event.internal.core; -import org.hibernate.event.internal.EntityCopyNotAllowedObserver; +import org.hibernate.event.internal.EntityCopyAllowedLoggedObserver; +import org.hibernate.event.internal.EntityCopyAllowedObserver; import org.hibernate.event.internal.EntityCopyObserver; /** - * Overrides {@link JpaMergeEventListener} to disallow merging multiple representations + * Overrides {@link JpaMergeEventListener} that allows merging multiple representations * of the same persistent entity. * * @author Gail Badner */ -public class JpaNoEntityCopiesMergeEventListener extends JpaMergeEventListener { +public class JpaEntityCopyAllowedMergeEventListener extends JpaMergeEventListener { @Override - protected EntityCopyObserver createDetachedEntityCopyObserver() { - return new EntityCopyNotAllowedObserver(); + protected EntityCopyObserver createEntityCopyObserver() { + return EntityCopyAllowedLoggedObserver.isDebugLoggingEnabled() ? + new EntityCopyAllowedLoggedObserver() : + new EntityCopyAllowedObserver(); } } diff --git a/hibernate-entitymanager/src/test/resources/log4j.properties b/hibernate-entitymanager/src/test/resources/log4j.properties index 50249d4a60..6d8e2cbe4a 100755 --- a/hibernate-entitymanager/src/test/resources/log4j.properties +++ b/hibernate-entitymanager/src/test/resources/log4j.properties @@ -38,5 +38,7 @@ log4j.logger.org.hibernate.tool.hbm2ddl=debug ### leakages when using DriverManagerConnectionProvider ### #log4j.logger.org.hibernate.connection.DriverManagerConnectionProvider=trace -### enable the following line to log multiple entity representations being merged for a persistent entity. -#log4j.logger.org.hibernate.event.internal.DefaultEntityCopyObserver=debug +### When entity copy merge functionality is enabled, the following will provide +### information about merged entity copies. +#log4j.logger.org.hibernate.event.internal.EntityCopyAllowedLoggedObserver=debug +