HHH-9106 : Merging multiple representations of the same entity
This commit is contained in:
parent
c7c5b015c1
commit
2daaf9a196
|
@ -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 );
|
||||
|
|
|
@ -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<String, Integer> 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<Object, Set<Object>> 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<Object> detachedEntitiesForManaged = null;
|
||||
if ( managedToMergeEntitiesXref == null ) {
|
||||
// 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 );
|
||||
Set<Object> detachedEntitiesForManaged = null;
|
||||
if ( managedToMergeEntitiesXref == null ) {
|
||||
// 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; 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() {
|
||||
|
@ -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<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() ) {
|
||||
Object managedEntity = entry.getKey();
|
||||
Set mergeEntities = entry.getValue();
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
}
|
||||
}
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<Category>) 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<Item>) 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();
|
||||
}
|
||||
|
|
|
@ -58,5 +58,7 @@ log4j.logger.org.hibernate.loader.plan2.exec.spi.EntityLoadQueryDetails=debug
|
|||
|
||||
log4j.logger.org.hibernate.engine.internal.StatisticalLoggingSessionEventListener=info
|
||||
|
||||
### 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
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue