HHH-9106 : Merging multiple representations of the same entity

This commit is contained in:
Gail Badner 2014-06-12 20:04:44 -07:00
parent c7c5b015c1
commit 2daaf9a196
12 changed files with 794 additions and 62 deletions

View File

@ -64,8 +64,8 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme
return ( (MergeContext) anything ).invertMap(); return ( (MergeContext) anything ).invertMap();
} }
protected EntityCopyObserver createDetachedEntityCopyObserver() { protected EntityCopyObserver createEntityCopyObserver() {
return new DefaultEntityCopyObserver(); return new EntityCopyNotAllowedObserver();
} }
/** /**
@ -76,7 +76,7 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme
* @throws HibernateException * @throws HibernateException
*/ */
public void onMerge(MergeEvent event) 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 ); final MergeContext mergeContext = new MergeContext( event.getSession(), entityCopyObserver );
try { try {
onMerge( event, mergeContext ); onMerge( event, mergeContext );

View File

@ -26,73 +26,98 @@ package org.hibernate.event.internal;
import java.util.IdentityHashMap; import java.util.IdentityHashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; 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.event.spi.EventSource;
import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreLogging;
import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.CoreMessageLogger;
import org.hibernate.internal.util.collections.IdentitySet; 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.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 * @author Gail Badner
*/ */
public class DefaultEntityCopyObserver implements EntityCopyObserver { public class EntityCopyAllowedLoggedObserver extends EntityCopyAllowedObserver {
private static final CoreMessageLogger LOG = CoreLogging.messageLogger( DefaultEntityCopyObserver.class ); private static final CoreMessageLogger LOG = CoreLogging.messageLogger( EntityCopyAllowedLoggedObserver.class );
// Tracks the number of entity copies per entity name.
private Map<String, Integer> countsByEntityName;
// managedToMergeEntitiesXref is only maintained for DEBUG logging so that a "nice" message // 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. // 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 // If no entity copies have been detected, managedToMergeEntitiesXref will remain null;
// will remain null;
private Map<Object, Set<Object>> managedToMergeEntitiesXref = null; private Map<Object, Set<Object>> managedToMergeEntitiesXref = null;
// key is the managed entity; // key is the managed entity;
// value is the set of representations being merged corresponding to the same managed result. // 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 @Override
public void entityCopyDetected( public void entityCopyDetected(
Object managedEntity, Object managedEntity,
Object mergeEntity1, Object mergeEntity1,
Object mergeEntity2, Object mergeEntity2,
EventSource session) { EventSource session) {
final String entityName = session.getEntityName( managedEntity );
LOG.trace( LOG.trace(
String.format( String.format(
"More than one representation of the same persistent entity being merged for: %s", "More than one representation of the same persistent entity being merged for: %s",
MessageHelper.infoString( MessageHelper.infoString(
session.getEntityName( managedEntity ), entityName,
session.getIdentifier( managedEntity ) session.getIdentifier( managedEntity )
) )
) )
); );
Set<Object> detachedEntitiesForManaged = null;
if ( LOG.isDebugEnabled() ) { if ( managedToMergeEntitiesXref == null ) {
// managedToMergeEntitiesXref is only maintained for DEBUG logging // This is the first time multiple representations have been found;
Set<Object> detachedEntitiesForManaged = null; // instantiate managedToMergeEntitiesXref.
if ( managedToMergeEntitiesXref == null ) { managedToMergeEntitiesXref = new IdentityHashMap<Object, Set<Object>>();
// This is the first time multiple representations have been found;
// instantiate managedToMergeEntitiesXref.
managedToMergeEntitiesXref = new IdentityHashMap<Object, Set<Object>>();
}
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 );
} }
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<String, Integer>();
}
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() { public void clear() {
@ -100,16 +125,34 @@ public class DefaultEntityCopyObserver implements EntityCopyObserver {
managedToMergeEntitiesXref.clear(); managedToMergeEntitiesXref.clear();
managedToMergeEntitiesXref = null; managedToMergeEntitiesXref = null;
} }
if ( countsByEntityName != null ) {
countsByEntityName.clear();
countsByEntityName = null;
}
} }
@Override @Override
public void topLevelMergeComplete(EventSource session) { public void topLevelMergeComplete(EventSource session) {
if ( !LOG.isDebugEnabled() ) { // Log the summary.
return; if ( countsByEntityName != null ) {
for ( Map.Entry<String, Integer> 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<Object,Set<Object>> entry : managedToMergeEntitiesXref.entrySet() ) { for ( Map.Entry<Object,Set<Object>> entry : managedToMergeEntitiesXref.entrySet() ) {
Object managedEntity = entry.getKey(); Object managedEntity = entry.getKey();
Set mergeEntities = entry.getValue(); Set mergeEntities = entry.getValue();

View File

@ -25,15 +25,17 @@
package org.hibernate.event.internal; 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. * multiple representations of the same persistent entity.
* *
* @author Gail Badner * @author Gail Badner
*/ */
public class NoEntityCopiesMergeEventListener extends DefaultMergeEventListener { public class EntityCopyAllowedMergeEventListener extends DefaultMergeEventListener {
@Override @Override
protected EntityCopyObserver createDetachedEntityCopyObserver() { protected EntityCopyObserver createEntityCopyObserver() {
return new EntityCopyNotAllowedObserver(); return EntityCopyAllowedLoggedObserver.isDebugLoggingEnabled() ?
new EntityCopyAllowedLoggedObserver() :
new EntityCopyAllowedObserver();
} }
} }

View File

@ -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.
}
}

View File

@ -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<Object,Object> input = new HashMap<Object,Object>();
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() {
}
}
}

View File

@ -84,4 +84,27 @@ public class Category {
", version=" + version + ", 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;
}
} }

View File

@ -29,7 +29,7 @@ import org.junit.Test;
import org.hibernate.Session; import org.hibernate.Session;
import org.hibernate.Transaction; 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.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType; import org.hibernate.event.spi.EventType;
import org.hibernate.testing.TestForIssue; 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 * 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 * @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 @Test
public void testCascadeFromDetachedToNonDirtyRepresentations() { public void testCascadeFromDetachedToNonDirtyRepresentations() {
Item item1 = new Item(); Item item1 = new Item();

View File

@ -30,6 +30,9 @@ import org.junit.Test;
import org.hibernate.Hibernate; import org.hibernate.Hibernate;
import org.hibernate.Session; import org.hibernate.Session;
import org.hibernate.Transaction; 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.FailureExpected;
import org.hibernate.testing.TestForIssue; import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; 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 @Test
@FailureExpected( jiraKey = "HHH-9240" ) @FailureExpected( jiraKey = "HHH-9240" )
public void testTopLevelUnidirOneToManyBackrefWithNewElement() { public void testTopLevelUnidirOneToManyBackrefWithNewElement() {

View File

@ -31,6 +31,9 @@ import org.hibernate.Hibernate;
import org.hibernate.Session; import org.hibernate.Session;
import org.hibernate.StaleObjectStateException; import org.hibernate.StaleObjectStateException;
import org.hibernate.Transaction; 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.FailureExpected;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; 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 @Test
public void testNestedDiffBasicProperty() { public void testNestedDiffBasicProperty() {
Item item1 = new Item(); Item item1 = new Item();
@ -564,6 +572,79 @@ public class MergeMultipleEntityRepresentationsTest extends BaseCoreFunctionalTe
cleanup(); 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 @Test
public void testTopLevelEntityNewerThanNested() { public void testTopLevelEntityNewerThanNested() {
Item item = new Item(); Item item = new Item();
@ -1111,19 +1192,27 @@ public class MergeMultipleEntityRepresentationsTest extends BaseCoreFunctionalTe
} }
for ( Category category : (List<Category>) s.createQuery( "from Category" ).list() ) { for ( Category category : (List<Category>) s.createQuery( "from Category" ).list() ) {
if ( category.getExampleItem() != null ) { Item exampleItem = category.getExampleItem();
if ( exampleItem != null ) {
category.setExampleItem( null ); category.setExampleItem( null );
exampleItem.setCategory( null );
s.delete( category ); s.delete( category );
s.delete (exampleItem );
} }
} }
for ( Item item : (List<Item>) s.createQuery( "from Item" ).list() ) { for ( Item item : (List<Item>) s.createQuery( "from Item" ).list() ) {
Category category = item.getCategory();
item.setCategory( null ); item.setCategory( null );
if ( category != null ) {
category.setExampleItem( null );
}
s.delete( item ); s.delete( item );
} }
s.createQuery( "delete from Item" ).executeUpdate(); s.createQuery( "delete from Item" ).executeUpdate();
s.getTransaction().commit(); s.getTransaction().commit();
s.close(); s.close();
} }

View File

@ -58,5 +58,7 @@ log4j.logger.org.hibernate.loader.plan2.exec.spi.EntityLoadQueryDetails=debug
log4j.logger.org.hibernate.engine.internal.StatisticalLoggingSessionEventListener=info log4j.logger.org.hibernate.engine.internal.StatisticalLoggingSessionEventListener=info
### enable the following line to log multiple entity representations being merged for a persistent entity. ### When entity copy merge functionality is enabled, the following will provide
#log4j.logger.org.hibernate.event.internal.DefaultEntityCopyObserver=debug ### information about merged entity copies.
#log4j.logger.org.hibernate.event.internal.EntityCopyAllowedLoggedObserver=debug

View File

@ -23,20 +23,23 @@
*/ */
package org.hibernate.jpa.event.internal.core; 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; 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. * of the same persistent entity.
* *
* @author Gail Badner * @author Gail Badner
*/ */
public class JpaNoEntityCopiesMergeEventListener extends JpaMergeEventListener { public class JpaEntityCopyAllowedMergeEventListener extends JpaMergeEventListener {
@Override @Override
protected EntityCopyObserver createDetachedEntityCopyObserver() { protected EntityCopyObserver createEntityCopyObserver() {
return new EntityCopyNotAllowedObserver(); return EntityCopyAllowedLoggedObserver.isDebugLoggingEnabled() ?
new EntityCopyAllowedLoggedObserver() :
new EntityCopyAllowedObserver();
} }
} }

View File

@ -38,5 +38,7 @@ log4j.logger.org.hibernate.tool.hbm2ddl=debug
### leakages when using DriverManagerConnectionProvider ### ### leakages when using DriverManagerConnectionProvider ###
#log4j.logger.org.hibernate.connection.DriverManagerConnectionProvider=trace #log4j.logger.org.hibernate.connection.DriverManagerConnectionProvider=trace
### enable the following line to log multiple entity representations being merged for a persistent entity. ### When entity copy merge functionality is enabled, the following will provide
#log4j.logger.org.hibernate.event.internal.DefaultEntityCopyObserver=debug ### information about merged entity copies.
#log4j.logger.org.hibernate.event.internal.EntityCopyAllowedLoggedObserver=debug