HHH-9106 : Merging multiple representations of the same entity merged

(cherry picked from commit 70532259fa)

Conflicts:
	hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java
	hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java
	hibernate-core/src/main/java/org/hibernate/event/internal/EventCache.java
	hibernate-core/src/test/java/org/hibernate/event/internal/EventCacheTest.java
	hibernate-core/src/test/java/org/hibernate/test/ops/AbstractOperationTestCase.java
	hibernate-core/src/test/java/org/hibernate/test/ops/Category.java
	hibernate-core/src/test/java/org/hibernate/test/ops/Hoarder.hbm.xml
	hibernate-core/src/test/java/org/hibernate/test/ops/Hoarder.java
	hibernate-core/src/test/java/org/hibernate/test/ops/Item.java
	hibernate-core/src/test/resources/log4j.properties
This commit is contained in:
Gail Badner 2014-06-12 01:46:23 -07:00
parent 5da9da4585
commit 6fdc9bac13
23 changed files with 3268 additions and 853 deletions

View File

@ -1322,6 +1322,10 @@ public class StatefulPersistenceContext implements PersistenceContext {
collectionPersister, collectionPersister,
unmergedInstance unmergedInstance
); );
LOG.debugf(
"Detached object being merged (corresponding with a managed entity) has a collection that [%s] the detached child.",
( found ? "contains" : "does not contain" )
);
} }
} }
@ -1348,6 +1352,10 @@ public class StatefulPersistenceContext implements PersistenceContext {
collectionPersister, collectionPersister,
mergeMap.get( proxy ) mergeMap.get( proxy )
); );
LOG.debugf(
"Detached proxy being merged has a collection that [%s] the managed child.",
(found ? "contains" : "does not contain")
);
if ( !found ) { if ( !found ) {
found = isFoundInParent( found = isFoundInParent(
propertyName, propertyName,
@ -1356,6 +1364,10 @@ public class StatefulPersistenceContext implements PersistenceContext {
collectionPersister, collectionPersister,
mergeMap.get( proxy ) mergeMap.get( proxy )
); );
LOG.debugf(
"Detached proxy being merged has a collection that [%s] the detached child being merged..",
(found ? "contains" : "does not contain")
);
} }
if ( found ) { if ( found ) {
return proxy.getHibernateLazyInitializer().getIdentifier(); return proxy.getHibernateLazyInitializer().getIdentifier();
@ -1402,10 +1414,14 @@ public class StatefulPersistenceContext implements PersistenceContext {
Object index = getIndexInParent(property, childEntity, persister, cp, parent); Object index = getIndexInParent(property, childEntity, persister, cp, parent);
if (index==null && mergeMap!=null) { if (index==null && mergeMap!=null) {
Object unmergedInstance = mergeMap.get(parent); final Object unMergedInstance = mergeMap.get( parent );
Object unmergedChild = mergeMap.get(childEntity); final Object unMergedChild = mergeMap.get( childEntity );
if ( unmergedInstance!=null && unmergedChild!=null ) { if ( unMergedInstance != null && unMergedChild != null ) {
index = getIndexInParent(property, unmergedChild, persister, cp, unmergedInstance); index = getIndexInParent( property, unMergedChild, persister, cp, unMergedInstance );
LOG.debugf(
"A detached object being merged (corresponding to a parent in parentsByChild) has an indexed collection that [%s] the detached child being merged. ",
( index != null ? "contains" : "does not contain" )
);
} }
} }
if (index!=null) { if (index!=null) {
@ -1421,15 +1437,18 @@ public class StatefulPersistenceContext implements PersistenceContext {
for ( Entry<Object, EntityEntry> me : reentrantSafeEntityEntries() ) { for ( Entry<Object, EntityEntry> me : reentrantSafeEntityEntries() ) {
EntityEntry ee = me.getValue(); EntityEntry ee = me.getValue();
if ( persister.isSubclassEntityName( ee.getEntityName() ) ) { if ( persister.isSubclassEntityName( ee.getEntityName() ) ) {
Object instance = me.getKey(); final Object instance = me.getKey();
Object index = getIndexInParent(property, childEntity, persister, cp, instance); Object index = getIndexInParent( property, childEntity, persister, cp, instance );
if ( index==null && mergeMap!=null ) {
if (index==null && mergeMap!=null) { final Object unMergedInstance = mergeMap.get( instance );
Object unmergedInstance = mergeMap.get(instance); final Object unMergedChild = mergeMap.get( childEntity );
Object unmergedChild = mergeMap.get(childEntity); if ( unMergedInstance != null && unMergedChild!=null ) {
if ( unmergedInstance!=null && unmergedChild!=null ) { index = getIndexInParent( property, unMergedChild, persister, cp, unMergedInstance );
index = getIndexInParent(property, unmergedChild, persister, cp, unmergedInstance); LOG.debugf(
"A detached object being merged (corresponding to a managed entity) has an indexed collection that [%s] the detached child being merged. ",
(index != null ? "contains" : "does not contain" )
);
} }
} }

View File

@ -0,0 +1,150 @@
/*
* 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.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;
/**
* @author Gail Badner
*/
public class DefaultEntityCopyObserver implements EntityCopyObserver {
private static final CoreMessageLogger LOG = CoreLogging.messageLogger( DefaultEntityCopyObserver.class );
// 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;
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.
@Override
public void entityCopyDetected(
Object managedEntity,
Object mergeEntity1,
Object mergeEntity2,
EventSource session) {
LOG.trace(
String.format(
"More than one representation of the same persistent entity being merged for: %s",
MessageHelper.infoString(
session.getEntityName( managedEntity ),
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 );
}
}
public void clear() {
if ( managedToMergeEntitiesXref != null ) {
managedToMergeEntitiesXref.clear();
managedToMergeEntitiesXref = null;
}
}
@Override
public void topLevelMergeComplete(EventSource session) {
if ( !LOG.isDebugEnabled() ) {
return;
}
if ( managedToMergeEntitiesXref != null && !managedToMergeEntitiesXref.isEmpty() ) {
for ( Map.Entry<Object,Set<Object>> entry : managedToMergeEntitiesXref.entrySet() ) {
Object managedEntity = entry.getKey();
Set mergeEntities = entry.getValue();
StringBuilder sb = new StringBuilder( "Found ")
.append( mergeEntities.size() )
.append( " entity representations of the same entity " )
.append(
MessageHelper.infoString(
session.getEntityName( managedEntity ),
session.getIdentifier( managedEntity )
)
)
.append( " being merged: " );
boolean first = true;
for ( Object mergeEntity : mergeEntities ) {
if ( first ) {
first = false;
}
else {
sb.append( ", " );
}
sb.append( getManagedOrDetachedEntityString( managedEntity, mergeEntity ) );
}
sb.append( "; resulting managed entity: [" ).append( managedEntity ).append( ']' );
LOG.debug( sb.toString());
}
}
}
private String getManagedOrDetachedEntityString(Object managedEntity, Object mergeEntity ) {
if ( mergeEntity == managedEntity) {
return "Managed: [" + mergeEntity + "]";
}
else {
return "Detached: [" + mergeEntity + "]";
}
}
}

View File

@ -1,7 +1,7 @@
/* /*
* Hibernate, Relational Persistence for Idiomatic Java * Hibernate, Relational Persistence for Idiomatic Java
* *
* Copyright (c) 2008-2011, Red Hat Inc. or third-party contributors as * Copyright (c) 2008-2014, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution * indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are * statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc. * distributed under license by Red Hat Inc.
@ -61,8 +61,12 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme
DefaultMergeEventListener.class.getName()); DefaultMergeEventListener.class.getName());
@Override @Override
protected Map getMergeMap(Object anything) { protected Map getMergeMap(Object anything) {
return ( ( EventCache ) anything ).invertMap(); return ( (MergeContext) anything ).invertMap();
}
protected EntityCopyObserver createDetachedEntityCopyObserver() {
return new DefaultEntityCopyObserver();
} }
/** /**
@ -72,10 +76,16 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme
* @throws HibernateException * @throws HibernateException
*/ */
public void onMerge(MergeEvent event) throws HibernateException { public void onMerge(MergeEvent event) throws HibernateException {
EventCache copyCache = new EventCache( event.getSession() ); final EntityCopyObserver entityCopyObserver = createDetachedEntityCopyObserver();
onMerge( event, copyCache ); final MergeContext mergeContext = new MergeContext( event.getSession(), entityCopyObserver );
copyCache.clear(); try {
copyCache = null; onMerge( event, mergeContext );
entityCopyObserver.topLevelMergeComplete( event.getSession() );
}
finally {
entityCopyObserver.clear();
mergeContext.clear();
}
} }
/** /**
@ -86,7 +96,7 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme
*/ */
public void onMerge(MergeEvent event, Map copiedAlready) throws HibernateException { public void onMerge(MergeEvent event, Map copiedAlready) throws HibernateException {
final EventCache copyCache = ( EventCache ) copiedAlready; final MergeContext copyCache = (MergeContext) copiedAlready;
final EventSource source = event.getSession(); final EventSource source = event.getSession();
final Object original = event.getOriginal(); final Object original = event.getOriginal();
@ -178,7 +188,7 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme
final EventSource source = event.getSession(); final EventSource source = event.getSession();
final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
( ( EventCache ) copyCache ).put( entity, entity, true ); //before cascade! ( (MergeContext) copyCache ).put( entity, entity, true ); //before cascade!
cascadeOnMerge(source, persister, entity, copyCache); cascadeOnMerge(source, persister, entity, copyCache);
copyValues(persister, entity, entity, source, copyCache); copyValues(persister, entity, entity, source, copyCache);
@ -203,7 +213,7 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme
persister.setIdentifier( copyCache.get( entity ), id, source ); persister.setIdentifier( copyCache.get( entity ), id, source );
} }
else { else {
( ( EventCache ) copyCache ).put( entity, source.instantiate( persister, id ), true ); //before cascade! ( (MergeContext) copyCache ).put( entity, source.instantiate( persister, id ), true ); //before cascade!
} }
final Object copy = copyCache.get( entity ); final Object copy = copyCache.get( entity );
@ -282,7 +292,7 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme
entityIsTransient(event, copyCache); entityIsTransient(event, copyCache);
} }
else { else {
( ( EventCache ) copyCache ).put( entity, result, true ); //before cascade! ( (MergeContext) copyCache ).put( entity, result, true ); //before cascade!
final Object target = source.getPersistenceContext().unproxy(result); final Object target = source.getPersistenceContext().unproxy(result);
if ( target == entity ) { if ( target == entity ) {

View File

@ -0,0 +1,72 @@
/*
* 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 org.hibernate.AssertionFailure;
import org.hibernate.event.spi.EventSource;
import org.hibernate.pretty.MessageHelper;
/**
* @author Gail Badner
*/
public class EntityCopyNotAllowedObserver implements EntityCopyObserver {
@Override
public void entityCopyDetected(
Object managedEntity,
Object mergeEntity1,
Object mergeEntity2,
EventSource session) {
if ( mergeEntity1 == managedEntity && mergeEntity2 == managedEntity) {
throw new AssertionFailure( "entity1 and entity2 are the same as managedEntity; must be different." );
}
final String managedEntityString = MessageHelper.infoString(
session.getEntityName( managedEntity ),
session.getIdentifier( managedEntity )
);
throw new IllegalStateException(
"Multiple representations of the same entity " + managedEntityString + " are being merged. " +
getManagedOrDetachedEntityString( managedEntity, mergeEntity1 ) + "; " +
getManagedOrDetachedEntityString( managedEntity, mergeEntity2 )
);
}
private String getManagedOrDetachedEntityString(Object managedEntity, Object entity ) {
if ( entity == managedEntity) {
return "Managed: [" + entity + "]";
}
else {
return "Detached: [" + entity + "]";
}
}
public void clear() {
// Nothing to do
}
@Override
public void topLevelMergeComplete(EventSource session) {
// Nothing to do
}
}

View File

@ -0,0 +1,56 @@
/*
* 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 org.hibernate.event.spi.EventSource;
/**
* An observer for detection of multiple entity representations for a persistent entity being merged.
*
* @author Gail Badner
*/
public interface EntityCopyObserver {
/**
* Called when more than one representation of the same persistent entity is being merged.
*
* @param managedEntity The managed entity in the persistence context (the merge result).
* @param mergeEntity1 A managed or detached entity being merged; must be non-null.
* @param mergeEntity2 A different managed or detached entity being merged; must be non-null.
* @param session The session.
*/
void entityCopyDetected(Object managedEntity, Object mergeEntity1, Object mergeEntity2, EventSource session);
/**
* Called when the top-level merge operation is complete.
*
* @param session The session
*/
void topLevelMergeComplete(EventSource session);
/**
* Called to clear any data stored in this EntityCopyObserver.
*/
void clear();
}

View File

@ -1,366 +0,0 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2008-2011, 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.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import org.hibernate.AssertionFailure;
import org.hibernate.event.spi.EventSource;
import org.hibernate.pretty.MessageHelper;
/**
* EventCache is a Map implementation that can be used by an event
* listener to keep track of entities involved in the operation
* being performed. This implementation allows entities to be added
* to the EventCache before the operation has cascaded to that
* entity.
* <p/>
* There are some restriction;
* <ul>
* <li>the same value cannot be associated with more than one key</li>
* <li>Methods that return collections (e.g., {@link #keySet()},
* {@link #values()}, {@link #entrySet()}) return an
* unnmodifiable view of the collection.</li>
* </ul>
* <p/>
* The following methods can be used by event listeners (and other
* classes) in the same package to add entities to an EventCache
* and indicate if the operation is being performed on the entity:<p/>
* {@link EventCache#put(Object entity, Object copy, boolean isOperatedOn)}
* <p/>
* The following method can be used by event listeners (and other
* classes) in the same package to indicate that the operation is being
* performed on an entity already in the EventCache:
* {@link EventCache#setOperatedOn(Object entity, boolean isOperatedOn)
*
* @author Gail Badner
*/
class EventCache implements Map {
private final EventSource session;
private Map<Object,Object> entityToCopyMap = new IdentityHashMap<Object,Object>(10);
// key is an entity involved with the operation performed by the listener;
// value can be either a copy of the entity or the entity itself
private Map<Object,Object> copyToEntityMap = new IdentityHashMap<Object,Object>( 10 );
// maintains the inverse of the entityToCopyMap for performance reasons.
private Map<Object,Boolean> entityToOperatedOnFlagMap = new IdentityHashMap<Object,Boolean>( 10 );
// key is an entity involved with the operation performed by the listener;
// value is a flag indicating if the listener explicitly operates on the entity
EventCache(EventSource session) {
this.session = session;
}
/**
* Clears the EventCache.
*/
public void clear() {
entityToCopyMap.clear();
copyToEntityMap.clear();
entityToOperatedOnFlagMap.clear();
}
/**
* Returns true if this EventCache contains a mapping for the specified entity.
* @param entity must be non-null
* @return true if this EventCache contains a mapping for the specified entity
* @throws NullPointerException if entity is null
*/
public boolean containsKey(Object entity) {
if ( entity == null ) {
throw new NullPointerException( "null entities are not supported by " + getClass().getName() );
}
return entityToCopyMap.containsKey( entity );
}
/**
* Returns true if this EventCache maps an entity to the specified copy.
* @param copy must be non-null
* @return true if this EventCache maps an entity to the specified copy
* @throws NullPointerException if copy is null
*/
public boolean containsValue(Object copy) {
if ( copy == null ) {
throw new NullPointerException( "null copies are not supported by " + getClass().getName() );
}
return copyToEntityMap.containsKey( copy );
}
/**
* Returns an unmodifiable set view of the entity-to-copy mappings contained in this EventCache.
* @return an unmodifiable set view of the entity-to-copy mappings contained in this EventCache
*
* @see {@link Collections#unmodifiableSet(java.util.Set)}
*/
public Set entrySet() {
return Collections.unmodifiableSet( entityToCopyMap.entrySet() );
}
/**
* Returns the copy to which this EventCache maps the specified entity.
* @param entity must be non-null
* @return the copy to which this EventCache maps the specified entity
* @throws NullPointerException if entity is null
*/
public Object get(Object entity) {
if ( entity == null ) {
throw new NullPointerException( "null entities are not supported by " + getClass().getName() );
}
return entityToCopyMap.get( entity );
}
/**
* Returns true if this EventCache contains no entity-copy mappings.
* @return true if this EventCache contains no entity-copy mappings
*/
public boolean isEmpty() {
return entityToCopyMap.isEmpty();
}
/**
* Returns an unmodifiable set view of the entities contained in this EventCache
* @return an unmodifiable set view of the entities contained in this EventCache
*
* @see {@link Collections#unmodifiableSet(java.util.Set)}
*/
public Set keySet() {
return Collections.unmodifiableSet( entityToCopyMap.keySet() );
}
/**
* Associates the specified entity with the specified copy in this EventCache;
* @param entity must be non-null
* @param copy must be non- null and must not be associated with any other entity in this EntityCache.
* @return previous copy associated with specified entity, or null if
* there was no mapping for entity.
* @throws NullPointerException if entity or copy is null
* @throws IllegalStateException if the specified copy is already associated with a different entity.
*/
public Object put(Object entity, Object copy) {
return put( entity, copy, Boolean.FALSE );
}
/**
* Associates the specified entity with the specified copy in this EventCache;
* @param entity must be non-null
* @param copy must be non- null and must not be associated with any other entity in this EntityCache.
* @param isOperatedOn indicates if the operation is performed on the entity.
*
* @return previous copy associated with specified entity, or null if
* there was no mapping for entity.
* @throws NullPointerException if entity or copy is null
* @throws IllegalStateException if the specified copy is already associated with a different entity.
*/
/* package-private */ Object put(Object entity, Object copy, boolean isOperatedOn) {
if ( entity == null || copy == null ) {
throw new NullPointerException( "null entities and copies are not supported by " + getClass().getName() );
}
Object oldCopy = entityToCopyMap.put( entity, copy );
Boolean oldOperatedOn = entityToOperatedOnFlagMap.put( entity, isOperatedOn );
Object oldEntity = copyToEntityMap.put( copy, entity );
if ( oldCopy == null ) {
if ( oldEntity != null ) {
throw new IllegalStateException(
"Error occurred while storing entity " + printEntity( entity ) + ". An entity copy " + printEntity( copy )
+ " was already assigned to a different entity " + printEntity( oldEntity ) + "."
);
}
if ( oldOperatedOn != null ) {
throw new IllegalStateException(
"EventCache#entityToOperatedOnFlagMap contains an entity " + printEntity( entity )
+ ", but EventCache#entityToCopyMap does not."
);
}
}
else {
if ( oldCopy != copy ) {
// Replaced an entity copy with a new copy; need to remove the oldCopy from copyToEntityMap
// to synch things up.
Object removedEntity = copyToEntityMap.remove( oldCopy );
if ( removedEntity != entity ) {
throw new IllegalStateException(
"Error occurred while storing entity " + printEntity( entity ) + ". An unexpected entity " + printEntity( removedEntity )
+ " was associated with the old entity copy " + printEntity( oldCopy ) + "."
);
}
if ( oldEntity != null ) {
throw new IllegalStateException(
"Error occurred while storing entity " + printEntity( entity ) + ". A new entity copy " + printEntity( copy )
+ " is already associated with a different entity " + printEntity( oldEntity ) + "."
);
}
}
else {
// Replaced an entity copy with the same copy in entityToCopyMap.
// Make sure that copy is associated with the same entity in copyToEntityMap.
if ( oldEntity != entity ) {
throw new IllegalStateException(
"An entity copy " + printEntity( copy ) + " was associated with a different entity "
+ printEntity( oldEntity ) + " than provided " + printEntity( entity ) + "."
);
}
}
if ( oldOperatedOn == null ) {
throw new IllegalStateException(
"EventCache#entityToCopyMap contained an entity " + printEntity( entity )
+ ", but EventCache#entityToOperatedOnFlagMap did not."
);
}
}
return oldCopy;
}
/**
* Copies all of the mappings from the specified map to this EventCache
* @param map keys and values must be non-null
* @throws NullPointerException if any map keys or values are null
*/
public void putAll(Map map) {
for ( Object o : map.entrySet() ) {
Entry entry = (Entry) o;
put( entry.getKey(), entry.getValue() );
}
}
/**
* Removes the mapping for this entity from this EventCache if it is present
* @param entity must be non-null
* @return previous value associated with specified entity, or null if there was no mapping for entity.
* @throws NullPointerException if entity is null
*/
public Object remove(Object entity) {
if ( entity == null ) {
throw new NullPointerException( "null entities are not supported by " + getClass().getName() );
}
Boolean oldOperatedOn = entityToOperatedOnFlagMap.remove( entity );
Object oldCopy = entityToCopyMap.remove( entity );
Object oldEntity = oldCopy != null ? copyToEntityMap.remove( oldCopy ) : null;
if ( oldCopy == null ) {
if ( oldOperatedOn != null ) {
throw new IllegalStateException(
"Removed entity " + printEntity( entity )
+ " from EventCache#entityToOperatedOnFlagMap, but EventCache#entityToCopyMap did not contain the entity."
);
}
}
else {
if ( oldEntity == null ) {
throw new IllegalStateException(
"Removed entity " + printEntity( entity )
+ " from EventCache#entityToCopyMap, but EventCache#copyToEntityMap did not contain the entity."
);
}
if ( oldOperatedOn == null ) {
throw new IllegalStateException(
"EventCache#entityToCopyMap contained an entity " + printEntity( entity )
+ ", but EventCache#entityToOperatedOnFlagMap did not."
);
}
if ( oldEntity != entity ) {
throw new IllegalStateException(
"An entity copy " + printEntity( oldCopy ) + " was associated with a different entity "
+ printEntity( oldEntity ) + " than provided " + printEntity( entity ) + "."
);
}
}
return oldCopy;
}
/**
* Returns the number of entity-copy mappings in this EventCache
* @return the number of entity-copy mappings in this EventCache
*/
public int size() {
return entityToCopyMap.size();
}
/**
* Returns an unmodifiable set view of the entity copies contained in this EventCache.
* @return an unmodifiable set view of the entity copies contained in this EventCache
*
* @see {@link Collections#unmodifiableSet(java.util.Set)}
*/
public Collection values() {
return Collections.unmodifiableCollection( entityToCopyMap.values() );
}
/**
* Returns true if the listener is performing the operation on the specified entity.
* @param entity must be non-null
* @return true if the listener is performing the operation on the specified entity.
* @throws NullPointerException if entity is null
*/
public boolean isOperatedOn(Object entity) {
if ( entity == null ) {
throw new NullPointerException( "null entities are not supported by " + getClass().getName() );
}
return entityToOperatedOnFlagMap.get( entity );
}
/**
* Set flag to indicate if the listener is performing the operation on the specified entity.
* @param entity must be non-null and this EventCache must contain a mapping for this entity
* @throws NullPointerException if entity is null
* @throws AssertionFailure if this EventCache does not contain a mapping for the specified entity
*/
/* package-private */ void setOperatedOn(Object entity, boolean isOperatedOn) {
if ( entity == null ) {
throw new NullPointerException( "null entities are not supported by " + getClass().getName() );
}
if ( ! entityToOperatedOnFlagMap.containsKey( entity ) ||
! entityToCopyMap.containsKey( entity ) ) {
throw new AssertionFailure( "called EventCache#setOperatedOn() for entity not found in EventCache" );
}
entityToOperatedOnFlagMap.put( entity, isOperatedOn );
}
/**
* Returns an unmodifiable map view of the copy-entity mappings
* @return an unmodifiable map view of the copy-entity mappings
*
* @see {@link Collections#unmodifiableMap(java.util.Map)}
*/
public Map invertMap() {
return Collections.unmodifiableMap( copyToEntityMap );
}
private String printEntity(Object entity) {
if ( session.getPersistenceContext().getEntry( entity ) != null ) {
return MessageHelper.infoString( session.getEntityName( entity ), session.getIdentifier( entity ) );
}
// Entity was not found in current persistence context. Use Object#toString() method.
return "[" + entity + "]";
}
}

View File

@ -0,0 +1,399 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2008-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.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import org.jboss.logging.Logger;
import org.hibernate.event.spi.EventSource;
import org.hibernate.pretty.MessageHelper;
/**
* MergeContext is a Map implementation that is intended to be used by a merge
* event listener to keep track of each entity being merged and their corresponding
* managed result. Entities to be merged may to be added to the MergeContext before
* the merge operation has cascaded to that entity.
*
* "Merge entity" and "mergeEntity" method parameter refer to an entity that is (or will be)
* merged via {@link org.hibernate.event.spi.EventSource#merge(Object mergeEntity)}.
*
* "Managed entity" and "managedEntity" method parameter refer to the managed entity that is
* the result of merging an entity.
*
* A merge entity can be transient, detached, or managed. If it is managed, then it must be
* the same as its associated entity result.
*
* If {@link #put(Object mergeEntity, Object managedEntity)} is called, and this
* MergeContext already contains an entry with a different entity as the key, but
* with the same (managedEntity) value, this means that multiple entity representations
* for the same persistent entity are being merged. If this happens,
* {@link org.hibernate.event.internal.EntityCopyObserver#entityCopyDetected(
* Object managedEntity, Object mergeEntity1, Object mergeEntity2, org.hibernate.event.spi.EventSource)}
* will be called. It is up to that method to determine the property course of
* action for this situation.
*
* There are several restrictions.
* <ul>
* <li>Methods that return collections (e.g., {@link #keySet()},
* {@link #values()}, {@link #entrySet()}) return an
* unnmodifiable view of the collection;</li>
* <li>If {@link #put(Object mergeEntity, Object) managedEntity} or
* {@link #put(Object mergeEntity, Object managedEntity, boolean isOperatedOn)}
* is executed and this MergeMap already contains a cross-reference for
* <code>mergeEntity</code>, then <code>managedEntity</code> must be the
* same as what is already associated with <code>mergeEntity</code> in this
* MergeContext.
* </li>
* <li>If {@link #putAll(Map map)} is executed, the previous restriction
* applies to each entry in the Map;</li>
* <li>The {@link #remove(Object)} operation is not supported;
* The only way to remove data from a MergeContext is by calling
* {@link #clear()};</li>
* <li>the Map returned by {@link #invertMap()} will only contain the
* managed-to-merge entity cross-reference to its "newest"
* (most recently added) merge entity.</li>
* </ul>
* <p>
* The following method is intended to be used by a merge event listener (and other
* classes) in the same package to add a merge entity and its corresponding
* managed entity to a MergeContext and indicate if the merge operation is
* being performed on the merge entity yet.<p/>
* {@link MergeContext#put(Object mergeEntity, Object managedEntity, boolean isOperatedOn)}
* <p/>
* The following method is intended to be used by a merge event listener (and other
* classes) in the same package to indicate whether the merge operation is being
* performed on a merge entity already in the MergeContext:
* {@link MergeContext#setOperatedOn(Object mergeEntity, boolean isOperatedOn)
*
* @author Gail Badner
*/
class MergeContext implements Map {
private static final Logger LOG = Logger.getLogger( MergeContext.class );
private final EventSource session;
private final EntityCopyObserver entityCopyObserver;
private Map<Object,Object> mergeToManagedEntityXref = new IdentityHashMap<Object,Object>(10);
// key is an entity to be merged;
// value is the associated managed entity (result) in the persistence context.
private Map<Object,Object> managedToMergeEntityXref = new IdentityHashMap<Object,Object>( 10 );
// maintains the inverse of the mergeToManagedEntityXref for performance reasons.
// key is the managed entity result in the persistence context.
// value is the associated entity to be merged; if multiple
// representations of the same persistent entity are added to the MergeContext,
// value will be the most recently added merge entity that is
// associated with the managed entity.
// TODO: merge mergeEntityToOperatedOnFlagMap into mergeToManagedEntityXref, since they have the same key.
// need to check if this would hurt performance.
private Map<Object,Boolean> mergeEntityToOperatedOnFlagMap = new IdentityHashMap<Object,Boolean>( 10 );
// key is a merge entity;
// value is a flag indicating if the merge entity is currently in the merge process.
MergeContext(EventSource session, EntityCopyObserver entityCopyObserver){
this.session = session;
this.entityCopyObserver = entityCopyObserver;
}
/**
* Clears the MergeContext.
*/
public void clear() {
mergeToManagedEntityXref.clear();
managedToMergeEntityXref.clear();
mergeEntityToOperatedOnFlagMap.clear();
}
/**
* Returns true if this MergeContext contains a cross-reference for the specified merge entity
* to a managed entity result.
*
* @param mergeEntity must be non-null
* @return true if this MergeContext contains a cross-reference for the specified merge entity
* @throws NullPointerException if mergeEntity is null
*/
public boolean containsKey(Object mergeEntity) {
if ( mergeEntity == null ) {
throw new NullPointerException( "null entities are not supported by " + getClass().getName() );
}
return mergeToManagedEntityXref.containsKey( mergeEntity );
}
/**
* Returns true if this MergeContext contains a cross-reference from the specified managed entity
* to a merge entity.
* @param managedEntity must be non-null
* @return true if this MergeContext contains a cross-reference from the specified managed entity
* to a merge entity
* @throws NullPointerException if managedEntity is null
*/
public boolean containsValue(Object managedEntity) {
if ( managedEntity == null ) {
throw new NullPointerException( "null copies are not supported by " + getClass().getName() );
}
return managedToMergeEntityXref.containsKey( managedEntity );
}
/**
* Returns an unmodifiable set view of the merge-to-managed entity cross-references contained in this MergeContext.
* @return an unmodifiable set view of the merge-to-managed entity cross-references contained in this MergeContext
*
* @see {@link Collections#unmodifiableSet(java.util.Set)}
*/
public Set entrySet() {
return Collections.unmodifiableSet( mergeToManagedEntityXref.entrySet() );
}
/**
* Returns the managed entity associated with the specified merge Entity.
* @param mergeEntity the merge entity; must be non-null
* @return the managed entity associated with the specified merge Entity
* @throws NullPointerException if mergeEntity is null
*/
public Object get(Object mergeEntity) {
if ( mergeEntity == null ) {
throw new NullPointerException( "null entities are not supported by " + getClass().getName() );
}
return mergeToManagedEntityXref.get( mergeEntity );
}
/**
* Returns true if this MergeContext contains no merge-to-managed entity cross-references.
* @return true if this MergeContext contains no merge-to-managed entity cross-references.
*/
public boolean isEmpty() {
return mergeToManagedEntityXref.isEmpty();
}
/**
* Returns an unmodifiable set view of the merge entities contained in this MergeContext
* @return an unmodifiable set view of the merge entities contained in this MergeContext
*
* @see {@link Collections#unmodifiableSet(java.util.Set)}
*/
public Set keySet() {
return Collections.unmodifiableSet( mergeToManagedEntityXref.keySet() );
}
/**
* Associates the specified merge entity with the specified managed entity result in this MergeContext.
* If this MergeContext already contains a cross-reference for <code>mergeEntity</code> when this
* method is called, then <code>managedEntity</code> must be the same as what is already associated
* with <code>mergeEntity</code>.
* <p/>
* This method assumes that the merge process is not yet operating on <code>mergeEntity</code>.
* Later when <code>mergeEntity</code> enters the merge process, {@link #setOperatedOn(Object, boolean)}
* should be called.
* <p/>
* @param mergeEntity the merge entity; must be non-null
* @param managedEntity the managed entity result; must be non-null
* @return previous managed entity associated with specified merge entity, or null if
* there was no mapping for mergeEntity.
* @throws NullPointerException if mergeEntity or managedEntity is null
* @throws IllegalArgumentException if <code>managedEntity</code> is not the same as the previous
* managed entity associated with <code>merge entity</code>
* @throws IllegalStateException if internal cross-references are out of sync,
*/
public Object put(Object mergeEntity, Object managedEntity) {
return put( mergeEntity, managedEntity, Boolean.FALSE );
}
/**
* Associates the specified merge entity with the specified managed entity in this MergeContext.
* If this MergeContext already contains a cross-reference for <code>mergeEntity</code> when this
* method is called, then <code>managedEntity</code> must be the same as what is already associated
* with <code>mergeEntity</code>.
*
* @param mergeEntity the mergge entity; must be non-null
* @param managedEntity the managed entity; must be non-null
* @param isOperatedOn indicates if the merge operation is performed on the mergeEntity.
*
* @return previous managed entity associated with specified merge entity, or null if
* there was no mapping for mergeEntity.
* @throws NullPointerException if mergeEntity or managedEntity is null
* @throws IllegalArgumentException if <code>managedEntity</code> is not the same as the previous
* managed entity associated with <code>mergeEntity</code>
* @throws IllegalStateException if internal cross-references are out of sync,
*/
/* package-private */ Object put(Object mergeEntity, Object managedEntity, boolean isOperatedOn) {
if ( mergeEntity == null || managedEntity == null ) {
throw new NullPointerException( "null merge and managed entities are not supported by " + getClass().getName() );
}
Object oldManagedEntity = mergeToManagedEntityXref.put( mergeEntity, managedEntity );
Boolean oldOperatedOn = mergeEntityToOperatedOnFlagMap.put( mergeEntity, isOperatedOn );
// If managedEntity already corresponds with a different merge entity, that means
// that there are multiple entities being merged that correspond with managedEntity.
// In the following, oldMergeEntity will be replaced with mergeEntity in managedToMergeEntityXref.
Object oldMergeEntity = managedToMergeEntityXref.put( managedEntity, mergeEntity );
if ( oldManagedEntity == null ) {
// this is a new mapping for mergeEntity in mergeToManagedEntityXref
if ( oldMergeEntity != null ) {
// oldMergeEntity was a different merge entity with the same corresponding managed entity;
entityCopyObserver.entityCopyDetected(
managedEntity,
mergeEntity,
oldMergeEntity,
session
);
}
if ( oldOperatedOn != null ) {
throw new IllegalStateException(
"MergeContext#mergeEntityToOperatedOnFlagMap contains an merge entity " + printEntity( mergeEntity )
+ ", but MergeContext#mergeToManagedEntityXref does not."
);
}
}
else {
// mergeEntity was already mapped in mergeToManagedEntityXref
if ( oldManagedEntity != managedEntity ) {
throw new IllegalArgumentException(
"Error occurred while storing a merge Entity " + printEntity( mergeEntity )
+ ". It was previously associated with managed entity " + printEntity( oldManagedEntity )
+ ". Attempted to replace managed entity with " + printEntity( managedEntity )
);
}
if ( oldOperatedOn == null ) {
throw new IllegalStateException(
"MergeContext#mergeToManagedEntityXref contained an mergeEntity " + printEntity( mergeEntity )
+ ", but MergeContext#mergeEntityToOperatedOnFlagMap did not."
);
}
}
return oldManagedEntity;
}
/**
* Copies all of the mappings from the specified Map to this MergeContext.
* The key and value for each entry in <code>map</code> is subject to the same
* restrictions as {@link #put(Object mergeEntity, Object managedEntity)}.
*
* This method assumes that the merge process is not yet operating on any merge entity
*
* @param map keys and values must be non-null
* @throws NullPointerException if any key or value is null
* @throws IllegalArgumentException if a key in <code>map</code> was already in this MergeContext
* but associated value in <code>map</code> is different from the previous value in this MergeContext.
* @throws IllegalStateException if internal cross-references are out of sync,
*/
public void putAll(Map map) {
for ( Object o : map.entrySet() ) {
Entry entry = (Entry) o;
put( entry.getKey(), entry.getValue() );
}
}
/**
* The remove operation is not supported.
* @param mergeEntity the merge entity.
* @throws UnsupportedOperationException if called.
*/
public Object remove(Object mergeEntity) {
throw new UnsupportedOperationException(
String.format( "Operation not supported: %s.remove()", getClass().getName() )
);
}
/**
* Returns the number of merge-to-managed entity cross-references in this MergeContext
* @return the number of merge-to-managed entity cross-references in this MergeContext
*/
public int size() {
return mergeToManagedEntityXref.size();
}
/**
* Returns an unmodifiable Set view of managed entities contained in this MergeContext.
* @return an unmodifiable Set view of managed entities contained in this MergeContext
*
* @see {@link Collections#unmodifiableSet(java.util.Set)}
*/
public Collection values() {
return Collections.unmodifiableSet( managedToMergeEntityXref.keySet() );
}
/**
* Returns true if the listener is performing the merge operation on the specified merge entity.
* @param mergeEntity the merge entity; must be non-null
* @return true if the listener is performing the merge operation on the specified merge entity;
* false, if there is no mapping for mergeEntity.
* @throws NullPointerException if mergeEntity is null
*/
public boolean isOperatedOn(Object mergeEntity) {
if ( mergeEntity == null ) {
throw new NullPointerException( "null merge entities are not supported by " + getClass().getName() );
}
final Boolean isOperatedOn = mergeEntityToOperatedOnFlagMap.get( mergeEntity );
return isOperatedOn == null ? false : isOperatedOn;
}
/**
* Set flag to indicate if the listener is performing the merge operation on the specified merge entity.
* @param mergeEntity must be non-null and this MergeContext must contain a cross-reference for mergeEntity
* to a managed entity
* @throws NullPointerException if mergeEntity is null
* @throws IllegalStateException if this MergeContext does not contain a a cross-reference for mergeEntity
*/
/* package-private */ void setOperatedOn(Object mergeEntity, boolean isOperatedOn) {
if ( mergeEntity == null ) {
throw new NullPointerException( "null entities are not supported by " + getClass().getName() );
}
if ( ! mergeEntityToOperatedOnFlagMap.containsKey( mergeEntity ) ||
! mergeToManagedEntityXref.containsKey( mergeEntity ) ) {
throw new IllegalStateException( "called MergeContext#setOperatedOn() for mergeEntity not found in MergeContext" );
}
mergeEntityToOperatedOnFlagMap.put( mergeEntity, isOperatedOn );
}
/**
* Returns an unmodifiable map view of the managed-to-merge entity
* cross-references.
*
* The returned Map will contain a cross-reference from each managed entity
* to the most recently associated merge entity that was most recently put in the MergeContext.
*
* @return an unmodifiable map view of the managed-to-merge entity cross-references.
*
* @see {@link Collections#unmodifiableMap(java.util.Map)}
*/
public Map invertMap() {
return Collections.unmodifiableMap( managedToMergeEntityXref );
}
private String printEntity(Object entity) {
if ( session.getPersistenceContext().getEntry( entity ) != null ) {
return MessageHelper.infoString( session.getEntityName( entity ), session.getIdentifier( entity ) );
}
// Entity was not found in current persistence context. Use Object#toString() method.
return "[" + entity + "]";
}
}

View File

@ -0,0 +1,39 @@
/*
* 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;
/**
* A {@link org.hibernate.event.spi.MergeEventListener} that does not allow merging
* multiple representations of the same persistent entity.
*
* @author Gail Badner
*/
public class NoEntityCopiesMergeEventListener extends DefaultMergeEventListener {
@Override
protected EntityCopyObserver createDetachedEntityCopyObserver() {
return new EntityCopyNotAllowedObserver();
}
}

View File

@ -1,460 +0,0 @@
/*
* 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.Session;
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.assertNull;
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 EventCache for performance improvement.
*
* @author Wim Ockerman @ CISCO
*/
public class EventCacheTest extends BaseCoreFunctionalTestCase {
private Session session = null;
private EventCache cache = null;
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] { Simple.class };
}
@Before
public void setUp() {
session = openSession();
cache = new EventCache( ( EventSource) session );
}
@After
public void tearDown() {
cache = null;
session.close();
session = null;
}
@Test
public void testEntityToCopyFillFollowedByCopyToEntityMapping() {
Object entity = new Simple( 1 );
Object copy = new Simple( 2 );
cache.put(entity, copy);
checkCacheConsistency( cache, 1 );
assertTrue( cache.containsKey( entity ) );
assertFalse( cache.containsKey( copy ) );
assertTrue( cache.containsValue( copy ) );
assertTrue( cache.invertMap().containsKey( copy ) );
assertFalse( cache.invertMap().containsKey( entity ) );
assertTrue( cache.invertMap().containsValue( entity ) );
cache.clear();
checkCacheConsistency( cache, 0 );
assertFalse(cache.containsKey(entity));
assertFalse(cache.invertMap().containsKey(copy));
}
@Test
public void testEntityToCopyFillFollowedByCopyToEntityMappingOnRemove() {
Object entity = new Simple( 1 );
Object copy = new Simple( 2 );
cache.put(entity, copy);
checkCacheConsistency( cache, 1 );
assertTrue(cache.containsKey(entity));
assertFalse( cache.containsKey( copy ) );
assertTrue( cache.invertMap().containsKey( copy ) );
assertFalse( cache.invertMap().containsKey( entity ) );
cache.remove( entity );
checkCacheConsistency( cache, 0 );
assertFalse(cache.containsKey(entity));
assertFalse(cache.invertMap().containsKey(copy));
}
@Test
public void testEntityToCopyFillFollowedByCopyToEntityUsingPutAll() {
Map<Object,Object> input = new HashMap<Object,Object>();
Object entity1 = new Simple( 1 );
//
Object copy1 = new Integer( 2 );
input.put(entity1, copy1);
Object entity2 = new Simple( 3 );
Object copy2 = new Integer( 2 );
input.put(entity2, copy2);
cache.putAll(input);
checkCacheConsistency( cache, 2 );
assertTrue(cache.containsKey(entity1));
assertFalse(cache.containsKey(copy1));
assertTrue(cache.containsKey(entity2));
assertFalse(cache.containsKey(copy2));
assertTrue(cache.invertMap().containsKey(copy1));
assertFalse(cache.invertMap().containsKey(entity1));
assertTrue(cache.invertMap().containsKey(copy2));
assertFalse(cache.invertMap().containsKey(entity2));
}
@Test
public void testEntityToCopyFillFollowedByCopyToEntityMappingUsingPutWithSetOperatedOnArg() {
Object entity = new Simple( 1 );
Object copy = new Simple( 2 );
cache.put(entity, copy, true);
checkCacheConsistency( cache, 1 );
assertTrue(cache.containsKey(entity));
assertFalse( cache.containsKey( copy ) );
assertTrue( cache.invertMap().containsKey( copy ) );
assertFalse( cache.invertMap().containsKey( entity ) );
cache.clear();
checkCacheConsistency( cache, 0 );
cache.put(entity, copy, false);
checkCacheConsistency( cache, 1 );
assertTrue(cache.containsKey(entity));
assertFalse(cache.containsKey(copy));
}
@Test
public void testEntityToCopyFillFollowedByIterateEntrySet() {
Object entity = new Simple( 1 );
Object copy = new Simple( 2 );
cache.put( entity, copy, true );
checkCacheConsistency( cache, 1 );
Iterator it = cache.entrySet().iterator();
assertTrue( it.hasNext() );
Map.Entry entry = ( Map.Entry ) it.next();
assertSame( entity, entry.getKey() );
assertSame( copy, entry.getValue() );
assertFalse( it.hasNext() );
}
@Test
public void testEntityToCopyFillFollowedByModifyEntrySet() {
Object entity = new Simple( 1 );
Object copy = new Simple( 2 );
cache.put( entity, copy, 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 testEntityToCopyFillFollowedByModifyKeys() {
Object entity = new Simple( 1 );
Object copy = new Simple( 2 );
cache.put( entity, copy, true );
Iterator it = cache.keySet().iterator();
try {
it.remove();
fail( "should have thrown UnsupportedOperationException" );
}
catch ( UnsupportedOperationException ex ) {
// expected
}
try {
cache.keySet().remove( entity );
fail( "should have thrown UnsupportedOperationException" );
}
catch ( UnsupportedOperationException ex ) {
// expected
}
Object newCopy = new Simple( 3 );
try {
cache.keySet().add( newCopy );
fail( "should have thrown UnsupportedOperationException" );
}
catch ( UnsupportedOperationException ex ) {
// expected
}
}
@Test
public void testEntityToCopyFillFollowedByModifyValues() {
Object entity = new Simple( 1 );
Object copy = new Simple( 2 );
cache.put( entity, copy, true );
Iterator it = cache.values().iterator();
try {
it.remove();
fail( "should have thrown UnsupportedOperationException" );
}
catch ( UnsupportedOperationException ex ) {
// expected
}
try {
cache.values().remove( copy );
fail( "should have thrown UnsupportedOperationException" );
}
catch ( UnsupportedOperationException ex ) {
// expected
}
Object newCopy = new Simple( 3 );
try {
cache.values().add( newCopy );
fail( "should have thrown UnsupportedOperationException" );
}
catch ( UnsupportedOperationException ex ) {
// expected
}
}
@Test
public void testEntityToCopyFillFollowedByModifyKeyOfEntrySetElement() {
Simple entity = new Simple( 1 );
Simple copy = new Simple( 0 );
cache.put(entity, copy, true);
Map.Entry entry = (Map.Entry) cache.entrySet().iterator().next();
( ( Simple ) entry.getKey() ).setValue( 2 );
assertEquals( 2, entity.getValue() );
checkCacheConsistency( cache, 1 );
entry = (Map.Entry) cache.entrySet().iterator().next();
assertSame( entity, entry.getKey() );
assertSame( copy, entry.getValue() );
}
@Test
public void testEntityToCopyFillFollowedByModifyValueOfEntrySetElement() {
Simple entity = new Simple( 1 );
Simple copy = new Simple( 0 );
cache.put(entity, copy, true);
Map.Entry entry = (Map.Entry) cache.entrySet().iterator().next();
( ( Simple ) entry.getValue() ).setValue( 2 );
assertEquals( 2, copy.getValue() );
checkCacheConsistency( cache, 1 );
entry = (Map.Entry) cache.entrySet().iterator().next();
assertSame( entity, entry.getKey() );
assertSame( copy, entry.getValue() );
}
@Test
public void testReplaceEntityCopy() {
Simple entity = new Simple( 1 );
Simple copy = new Simple( 0 );
cache.put(entity, copy);
Simple copyNew = new Simple( 0 );
assertSame( copy, cache.put( entity, copyNew ) );
assertSame( copyNew, cache.get( entity ) );
checkCacheConsistency( cache, 1 );
copy = copyNew;
copyNew = new Simple( 1 );
assertSame( copy, cache.put( entity, copyNew ) );
assertSame( copyNew, cache.get( entity ) );
checkCacheConsistency( cache, 1 );
}
@Test
public void testCopyAssociatedWithNewAndExistingEntity() {
session.getTransaction().begin();
Simple entity = new Simple( 1 );
Simple copy = new Simple( 0 );
session.persist( entity );
cache.put(entity, copy);
session.flush();
try {
cache.put( new Simple( 1 ), copy );
fail( "should have thrown IllegalStateException");
}
catch( IllegalStateException ex ) {
// expected
assertTrue( ex.getMessage().startsWith( "Error occurred while storing entity [org.hibernate.event.internal.EventCacheTest$Simple@" ) );
}
session.getTransaction().rollback();
}
@Test
public void testCopyAssociatedWith2ExistingEntities() {
session.getTransaction().begin();
Simple entity1 = new Simple( 1 );
session.persist( entity1 );
Simple copy1 = new Simple( 1 );
cache.put(entity1, copy1);
Simple entity2 = new Simple( 2 );
session.persist( entity2 );
Simple copy2 = new Simple( 2 );
cache.put( entity2, copy2 );
session.flush();
try {
cache.put( entity1, copy2 );
fail( "should have thrown IllegalStateException");
}
catch( IllegalStateException ex ) {
// expected
assertTrue( ex.getMessage().startsWith( "Error occurred while storing entity [org.hibernate.event.internal.EventCacheTest$Simple#1]." ) );
}
session.getTransaction().rollback();
}
@Test
public void testRemoveNonExistingEntity() {
assertNull( cache.remove( new Simple( 1 ) ) );
}
private void checkCacheConsistency(EventCache 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;
}
}
}

View File

@ -0,0 +1,87 @@
/*
* 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.test.ops;
import java.util.HashSet;
import java.util.Set;
/**
* @author Gail Badner
*/
public class Category {
private Long id;
private String name;
private Item exampleItem;
private int version;
private Set<SubCategory> subCategories = new HashSet<SubCategory>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Item getExampleItem() {
return exampleItem;
}
public void setExampleItem(Item exampleItem) {
this.exampleItem = exampleItem;
}
public Set<SubCategory> getSubCategories() {
return subCategories;
}
public void setSubCategories(Set<SubCategory> subCategories) {
this.subCategories = subCategories;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
@Override
public String toString() {
return "Category{" +
"id=" + id +
", name='" + name + '\'' +
", version=" + version +
'}';
}
}

View File

@ -0,0 +1,72 @@
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<!--
-->
<hibernate-mapping package="org.hibernate.test.ops">
<class name="Hoarder">
<id name="id">
<generator class="native"/>
</id>
<property name="name"/>
<many-to-one name="favoriteItem" cascade="persist,merge,save-update" />
<set name="items" cascade="persist,merge,save-update">
<key column="HOARDER_ID"/>
<one-to-many class="Item" />
</set>
</class>
<class name="Item">
<id name="id">
<generator class="native"/>
</id>
<version name="version"/>
<property name="name" not-null="true" />
<many-to-one name="category" cascade="persist,merge,save-update"/>
<set name="colors">
<key column="itemId"/>
<element type="string" not-null="true"/>
</set>
<list name="subItemsBackref" lazy="true" cascade="persist,merge,save-update">
<key column="parentItem" not-null="true"/>
<index column="index"/>
<one-to-many class="SubItem"/>
</list>
</class>
<class name="SubItem">
<id name="id">
<generator class="native"/>
</id>
<property name="name" not-null="true" />
</class>
<class name="Category">
<id name="id">
<generator class="native"/>
</id>
<version name="version" />
<property name="name" not-null="true" />
<many-to-one name="exampleItem" cascade="persist,merge,save-update" />
<set name="subCategories" lazy="true" cascade="persist,merge,save-update">
<key column="parentCategory" not-null="false"/>
<one-to-many class="SubCategory"/>
</set>
</class>
<class name="SubCategory">
<id name="id">
<generator class="native"/>
</id>
<property name="name" not-null="true" />
</class>
</hibernate-mapping>

View File

@ -0,0 +1,78 @@
/*
* 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.test.ops;
import java.util.HashSet;
import java.util.Set;
/**
* @author Gail Badner
*/
public class Hoarder {
private Long id;
private String name;
private Item favoriteItem;
private Set<Item> items = new HashSet<Item>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Item getFavoriteItem() {
return favoriteItem;
}
public void setFavoriteItem(Item favoriteItem) {
this.favoriteItem = favoriteItem;
}
public Set<Item> getItems() {
return items;
}
public void setItems(Set<Item> items) {
this.items = items;
}
@Override
public String toString() {
return "Hoarder{" +
"id=" + id +
", name='" + name + '\'' +
", favoriteItem=" + favoriteItem +
'}';
}
}

View File

@ -0,0 +1,72 @@
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC
"-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">
<!--
-->
<hibernate-mapping package="org.hibernate.test.ops">
<class name="Hoarder">
<id name="id">
<generator class="native"/>
</id>
<property name="name"/>
<many-to-one name="favoriteItem" cascade="persist,merge,save-update" />
<set name="items" cascade="persist,merge,save-update">
<key column="HOARDER_ID"/>
<one-to-many class="Item" />
</set>
</class>
<class name="Item">
<id name="id">
<generator class="native"/>
</id>
<version name="version"/>
<property name="name" not-null="true" />
<many-to-one name="category" cascade="persist,merge,save-update"/>
<set name="colors">
<key column="itemId"/>
<element type="string" not-null="true"/>
</set>
<list name="subItemsBackref" lazy="true" cascade="persist,merge,save-update,delete-orphan">
<key column="parentItem" not-null="true"/>
<index column="index"/>
<one-to-many class="SubItem"/>
</list>
</class>
<class name="SubItem">
<id name="id">
<generator class="native"/>
</id>
<property name="name" not-null="true" />
</class>
<class name="Category">
<id name="id">
<generator class="native"/>
</id>
<version name="version" />
<property name="name" not-null="true" />
<many-to-one name="exampleItem" cascade="persist,merge,save-update" />
<set name="subCategories" lazy="true" cascade="persist,merge,save-update,delete-orphan">
<key column="parentCategory" not-null="false"/>
<one-to-many class="SubCategory"/>
</set>
</class>
<class name="SubCategory">
<id name="id">
<generator class="native"/>
</id>
<property name="name" not-null="true" />
</class>
</hibernate-mapping>

View File

@ -0,0 +1,123 @@
/*
* 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.test.ops;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @author Gail Badner
*/
public class Item {
private Long id;
private int version;
private String name;
private Category category;
private List<SubItem> subItemsBackref = new ArrayList<SubItem>();
private Set<String> colors = new HashSet<String>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public int getVersion() {
return version;
}
public void setVersion(int version) {
this.version = version;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
public Set<String> getColors() {
return colors;
}
public void setColors(Set<String> colors) {
this.colors = colors;
}
public List<SubItem> getSubItemsBackref() {
return subItemsBackref;
}
public void setSubItemsBackref(List<SubItem> subItemsBackref) {
this.subItemsBackref = subItemsBackref;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
Item item = (Item) o;
if ( !name.equals( item.name ) ) {
return false;
}
return true;
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public String toString() {
return "Item{" +
"id=" + id +
", version=" + version +
", name='" + name + '\'' +
//", category=" + category +
//", subItems=" + subItems +
'}';
}
}

View File

@ -0,0 +1,181 @@
/*
* 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.test.ops;
import java.util.List;
import org.junit.Test;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.event.internal.NoEntityCopiesMergeEventListener;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import static junit.framework.TestCase.fail;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
/**
* Tests merging multiple detached representations of the same entity using
* a MergeEventListener that does not allow this.
*
* @author Gail Badner
*/
@TestForIssue( jiraKey = "HHH-9106")
public class MergeMultipleEntityRepresentationsNotAllowedTest extends BaseCoreFunctionalTestCase {
public String[] getMappings() {
return new String[] {
"ops/Hoarder.hbm.xml"
};
}
protected void afterSessionFactoryBuilt() {
EventListenerRegistry registry = sessionFactory().getServiceRegistry().getService( EventListenerRegistry.class );
registry.setListeners( EventType.MERGE, new NoEntityCopiesMergeEventListener() );
}
@Test
public void testCascadeFromDetachedToNonDirtyRepresentations() {
Item item1 = new Item();
item1.setName( "item1" );
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_1 and item1_2 are unmodified representations of the same persistent entity.
assertFalse( item1 == item1_1 );
assertTrue( item1.equals( item1_1 ) );
// Update hoarder (detached) to references both representations.
hoarder.getItems().add( item1 );
hoarder.setFavoriteItem( item1_1 );
s = openSession();
tx = s.beginTransaction();
try {
hoarder = (Hoarder) s.merge( hoarder );
fail( "should have failed due IllegalStateException");
}
catch (IllegalStateException ex) {
//expected
}
finally {
tx.rollback();
s.close();
}
cleanup();
}
@Test
public void testTopLevelManyToOneManagedNestedIsDetached() {
Item item1 = new Item();
item1.setName( "item1 name" );
Category category = new Category();
category.setName( "category" );
item1.setCategory( category );
category.setExampleItem( item1 );
Session s = openSession();
Transaction tx = s.beginTransaction();
s.persist( item1 );
tx.commit();
s.close();
// get another representation of item1
s = openSession();
tx = s.beginTransaction();
Item item1_1 = (Item) s.get( Item.class, item1.getId() );
tx.commit();
s.close();
s = openSession();
tx = s.beginTransaction();
Item item1Merged = (Item) s.merge( item1 );
item1Merged.setCategory( category );
category.setExampleItem( item1_1 );
// now item1Merged is managed and it has a nested detached item
try {
s.merge( item1Merged );
fail( "should have failed due IllegalStateException");
}
catch (IllegalStateException ex) {
//expected
}
finally {
tx.rollback();
s.close();
}
cleanup();
}
@SuppressWarnings( {"unchecked"})
private void cleanup() {
Session s = openSession();
s.beginTransaction();
s.createQuery( "delete from SubItem" ).executeUpdate();
for ( Hoarder hoarder : (List<Hoarder>) s.createQuery( "from Hoarder" ).list() ) {
hoarder.getItems().clear();
s.delete( hoarder );
}
for ( Category category : (List<Category>) s.createQuery( "from Category" ).list() ) {
if ( category.getExampleItem() != null ) {
category.setExampleItem( null );
s.delete( category );
}
}
for ( Item item : (List<Item>) s.createQuery( "from Item" ).list() ) {
item.setCategory( null );
s.delete( item );
}
s.createQuery( "delete from Item" ).executeUpdate();
s.getTransaction().commit();
s.close();
}
}

View File

@ -0,0 +1,531 @@
/*
* 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.test.ops;
import java.util.List;
import org.junit.Test;
import org.hibernate.Hibernate;
import org.hibernate.Session;
import org.hibernate.Transaction;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/**
* Tests merging multiple detached representations of the same entity.
*
* @author Gail Badner
*/
public class MergeMultipleEntityRepresentationsOrphanDeleteTest extends BaseCoreFunctionalTestCase {
public String[] getMappings() {
return new String[] {
"ops/HoarderOrphanDelete.hbm.xml"
};
}
@Test
//@FailureExpected( jiraKey = "HHH-9106" )
public void testTopLevelUnidirOneToManyBackrefWithNewElement() {
Item item1 = new Item();
item1.setName( "item1 name" );
SubItem subItem1 = new SubItem();
subItem1.setName( "subItem1 name" );
item1.getSubItemsBackref().add( subItem1 );
Session s = openSession();
Transaction tx = s.beginTransaction();
s.persist( item1 );
tx.commit();
s.close();
// get another representation of item1
s = openSession();
tx = s.beginTransaction();
Item item1_1 = (Item) s.get( Item.class, item1.getId() );
tx.commit();
s.close();
assertFalse( Hibernate.isInitialized( item1_1.getSubItemsBackref() ) );
Category category = new Category();
category.setName( "category" );
SubItem subItem2 = new SubItem();
subItem2.setName( "subItem2 name" );
item1.getSubItemsBackref().add( subItem2 );
item1.setCategory( category );
category.setExampleItem( item1_1 );
s = openSession();
tx = s.beginTransaction();
// The following will fail due to PropertyValueException because item1 will
// be removed from the inverted merge map when the operation cascades to item1_1.
Item item1Merged = (Item) s.merge( item1 );
// top-level collection should win
assertEquals( 2, item1.getSubItemsBackref().size() );
tx.commit();
s.close();
s = openSession();
tx = s.beginTransaction();
item1 = (Item) s.get( Item.class, item1.getId() );
assertEquals( 2, item1.getSubItemsBackref().size() );
tx.commit();
s.close();
cleanup();
}
@Test
@TestForIssue( jiraKey = "HHH-9171" )
public void testNestedUnidirOneToManyBackrefWithNewElement() {
Item item1 = new Item();
item1.setName( "item1 name" );
SubItem subItem1 = new SubItem();
subItem1.setName( "subItem1 name" );
item1.getSubItemsBackref().add( subItem1 );
Session s = openSession();
Transaction tx = s.beginTransaction();
s.persist( item1 );
tx.commit();
s.close();
// get another representation of item1
s = openSession();
tx = s.beginTransaction();
Item item1_1 = (Item) s.get( Item.class, item1.getId() );
Hibernate.initialize( item1_1.getSubItemsBackref() );
tx.commit();
s.close();
Category category = new Category();
category.setName( "category" );
item1.setCategory( category );
// Add a new SubItem to the Item representation that will be in a nested association.
SubItem subItem2 = new SubItem();
subItem2.setName( "subItem2 name" );
item1_1.getSubItemsBackref().add( subItem2 );
category.setExampleItem( item1_1 );
s = openSession();
tx = s.beginTransaction();
Item item1Merged = (Item) s.merge( item1 );
// The new SubItem was persisted because it is added to the nested item's collection.
// item1.subItems overwrites the collection (which should remove the new SubItem),
// Even though cascade includes "delete-orphan", the new SubItem is not
// deleted. This is because JPA spec says,"If the entity being orphaned is a detached,
// new, or removed entity, the semantics of orphanRemoval do not apply."
// Because the collection key is non-nullable, SubItem still has a backref to the
// same collection owner.
// gb: Shouldn't the collection key be updated to null causing a ConstraintViolationException???
// Note that the collection resulting from the merge looks OK here.
assertEquals( 1, item1Merged.getSubItemsBackref().size() );
assertEquals( "subItem1 name", item1Merged.getSubItemsBackref().get( 0 ).getName() );
tx.commit();
s.close();
s = openSession();
tx = s.beginTransaction();
item1 = (Item) s.get( Item.class, item1.getId() );
// The following fails because that new SubItem from the previous transaction gets loaded
// into the collection because it still has the collection key set. <== gb: bug?
assertEquals( 1, item1.getSubItemsBackref().size() );
assertEquals( "subItem1 name", item1.getSubItemsBackref().get( 0 ).getName() );
tx.commit();
s.close();
cleanup();
}
@Test
//@FailureExpected( jiraKey = "HHH-9106" )
public void testTopLevelUnidirOneToManyBackrefWithRemovedElement() {
Item item1 = new Item();
item1.setName( "item1 name" );
SubItem subItem1 = new SubItem();
subItem1.setName( "subItem1 name" );
item1.getSubItemsBackref().add( subItem1 );
SubItem subItem2 = new SubItem();
subItem2.setName( "subItem2 name" );
item1.getSubItemsBackref().add( subItem2 );
Session s = openSession();
Transaction tx = s.beginTransaction();
s.persist( item1 );
tx.commit();
s.close();
// get another representation of item1
s = openSession();
tx = s.beginTransaction();
Item item1_1 = (Item) s.get( Item.class, item1.getId() );
tx.commit();
s.close();
assertFalse( Hibernate.isInitialized( item1_1.getSubItemsBackref() ) );
Category category = new Category();
category.setName( "category" );
item1.setCategory( category );
category.setExampleItem( item1_1 );
// remove subItem1 from top-level Item
item1.getSubItemsBackref().remove( subItem1 );
s = openSession();
tx = s.beginTransaction();
Item item1Merged = (Item) s.merge( item1 );
// top-level collection should win
assertEquals( 1, item1Merged.getSubItemsBackref().size() );
tx.commit();
s.close();
s = openSession();
tx = s.beginTransaction();
item1 = (Item) s.get( Item.class, item1.getId() );
assertEquals( 1, item1.getSubItemsBackref().size() );
subItem1 = (SubItem) s.get( SubItem.class, subItem1.getId() );
assertNull( subItem1 );
tx.commit();
cleanup();
}
@Test
//@FailureExpected( jiraKey = "HHH-9171" )
public void testNestedUnidirOneToManyBackrefWithRemovedElement() {
Item item1 = new Item();
item1.setName( "item1 name" );
SubItem subItem1 = new SubItem();
subItem1.setName( "subItem1 name" );
item1.getSubItemsBackref().add( subItem1 );
SubItem subItem2 = new SubItem();
subItem2.setName( "subItem2 name" );
item1.getSubItemsBackref().add( subItem2 );
Session s = openSession();
Transaction tx = s.beginTransaction();
s.persist( item1 );
tx.commit();
s.close();
// get another representation of item1
s = openSession();
tx = s.beginTransaction();
Item item1_1 = (Item) s.get( Item.class, item1.getId() );
Hibernate.initialize( item1_1.getSubItemsBackref() );
tx.commit();
s.close();
// remove subItem1 from the nested Item
item1_1.getSubItemsBackref().remove( subItem1 );
Category category = new Category();
category.setName( "category" );
item1.setCategory( category );
category.setExampleItem( item1_1 );
s = openSession();
tx = s.beginTransaction();
Item item1Merged = (Item) s.merge( item1 );
// collection from top-level Item should win
assertEquals( 2, item1Merged.getSubItemsBackref().size() );
assertTrue( item1Merged.getSubItemsBackref().contains( subItem1 ) );
assertTrue( item1Merged.getSubItemsBackref().contains( subItem2 ) );
tx.commit();
s.close();
s = openSession();
tx = s.beginTransaction();
item1 = (Item) s.get( Item.class, item1.getId() );
assertEquals( 2, item1.getSubItemsBackref().size() );
assertTrue( item1.getSubItemsBackref().contains( subItem1 ) );
assertTrue( item1.getSubItemsBackref().contains( subItem2 ) );
tx.commit();
s.close();
cleanup();
}
@Test
//@FailureExpected( jiraKey = "HHH-9106" )
public void testTopLevelUnidirOneToManyNoBackrefWithNewElement() {
Category category1 = new Category();
category1.setName( "category1 name" );
SubCategory subCategory1 = new SubCategory();
subCategory1.setName( "subCategory1 name" );
category1.getSubCategories().add( subCategory1 );
Session s = openSession();
Transaction tx = s.beginTransaction();
s.persist( category1 );
tx.commit();
s.close();
// get another representation of category1
s = openSession();
tx = s.beginTransaction();
Category category1_1 = (Category) s.get( Category.class, category1.getId() );
tx.commit();
s.close();
assertFalse( Hibernate.isInitialized( category1_1.getSubCategories() ) );
SubCategory subCategory2 = new SubCategory();
subCategory2.setName( "subCategory2 name" );
category1.getSubCategories().add( subCategory2 );
Item item = new Item();
item.setName( "item" );
category1.setExampleItem( item );
item.setCategory( category1_1 );
s = openSession();
tx = s.beginTransaction();
Category category1Merged = (Category) s.merge( category1 );
assertEquals( 2, category1Merged.getSubCategories().size() );
tx.commit();
s.close();
s = openSession();
tx = s.beginTransaction();
category1 = (Category) s.get( Category.class, category1.getId() );
assertEquals( 2, category1.getSubCategories().size() );
tx.commit();
s.close();
cleanup();
}
@Test
public void testNestedUnidirOneToManyNoBackrefWithNewElement() {
Category category1 = new Category();
category1.setName( "category1 name" );
SubCategory subCategory1 = new SubCategory();
subCategory1.setName( "subCategory1 name" );
category1.getSubCategories().add( subCategory1 );
Session s = openSession();
Transaction tx = s.beginTransaction();
s.persist( category1 );
tx.commit();
s.close();
// get another representation of category1
s = openSession();
tx = s.beginTransaction();
Category category1_1 = (Category) s.get( Category.class, category1.getId() );
Hibernate.initialize( category1_1.getSubCategories() );
tx.commit();
s.close();
SubCategory subCategory2 = new SubCategory();
subCategory2.setName( "subCategory2 name" );
category1_1.getSubCategories().add( subCategory2 );
Item item = new Item();
item.setName( "item" );
category1.setExampleItem( item );
item.setCategory( category1_1 );
s = openSession();
tx = s.beginTransaction();
// top-level collection should still have just 1 element.
// copy of subcategory2 should still be persisted even though cascade includes delete-orphan.
// This is because in Section 2.9, Entity Relationships, the JPA 2.1 spec says:
// "If the entity being orphaned is a detached, new, or removed entity, the semantics of orphanRemoval do not apply.".
Category category1Merged = (Category) s.merge( category1 );
assertEquals( 1, category1Merged.getSubCategories().size() );
assertTrue( category1Merged.getSubCategories().contains( subCategory1 ) );
tx.commit();
s.close();
s = openSession();
tx = s.beginTransaction();
category1 = (Category) s.get( Category.class, category1.getId() );
assertEquals( 1, category1.getSubCategories().size() );
assertTrue( category1.getSubCategories().contains( subCategory1 ) );
subCategory2 = (SubCategory) s.createQuery( "from SubCategory sc where sc.name = 'subCategory2 name'" ).uniqueResult();
assertNotNull( subCategory2 );
tx.commit();
s.close();
cleanup();
}
@Test
//@FailureExpected( jiraKey = "HHH-9106" )
public void testTopLevelUnidirOneToManyNoBackrefWithRemovedElement() {
Category category1 = new Category();
category1.setName( "category1 name" );
SubCategory subCategory1 = new SubCategory();
subCategory1.setName( "subCategory1 name" );
category1.getSubCategories().add( subCategory1 );
SubCategory subCategory2 = new SubCategory();
subCategory2.setName( "subCategory2 name" );
category1.getSubCategories().add( subCategory2 );
Session s = openSession();
Transaction tx = s.beginTransaction();
s.persist( category1 );
tx.commit();
s.close();
// get another representation of category1
s = openSession();
tx = s.beginTransaction();
Category category1_1 = (Category) s.get( Category.class, category1.getId() );
tx.commit();
s.close();
assertFalse( Hibernate.isInitialized( category1_1.getSubCategories() ) );
Item item = new Item();
item.setName( "item" );
category1.setExampleItem( item );
item.setCategory( category1_1 );
category1.getSubCategories().remove( subCategory1 );
s = openSession();
tx = s.beginTransaction();
Category category1Merged = (Category) s.merge( category1 );
assertEquals( 1, category1Merged.getSubCategories().size() );
assertTrue( category1Merged.getSubCategories().contains( subCategory2 ) );
tx.commit();
s.close();
s = openSession();
tx = s.beginTransaction();
category1 = (Category) s.get( Category.class, category1.getId() );
assertEquals( 1, category1.getSubCategories().size() );
assertTrue( category1.getSubCategories().contains( subCategory2 ) );
subCategory1 = (SubCategory) s.get( SubCategory.class, subCategory1.getId() );
assertNull( subCategory1 );
tx.commit();
s.close();
cleanup();
}
@Test
//@FailureExpected( jiraKey = "HHH-9171" )
public void testNestedUnidirOneToManyNoBackrefWithRemovedElement() {
Category category1 = new Category();
category1.setName( "category1 name" );
SubCategory subCategory1 = new SubCategory();
subCategory1.setName( "subCategory1 name" );
category1.getSubCategories().add( subCategory1 );
SubCategory subCategory2 = new SubCategory();
subCategory2.setName( "subCategory2 name" );
category1.getSubCategories().add( subCategory2 );
Session s = openSession();
Transaction tx = s.beginTransaction();
s.persist( category1 );
tx.commit();
s.close();
// get another representation of category1
s = openSession();
tx = s.beginTransaction();
Category category1_1 = (Category) s.get( Category.class, category1.getId() );
Hibernate.initialize( category1_1.getSubCategories() );
tx.commit();
s.close();
category1_1.getSubCategories().remove( subCategory2 );
Item item = new Item();
item.setName( "item" );
category1.setExampleItem( item );
item.setCategory( category1_1 );
s = openSession();
tx = s.beginTransaction();
// top-level collection should still have 2 elements.
Category category1Merged = (Category) s.merge( category1 );
assertEquals( 2, category1Merged.getSubCategories().size() );
assertTrue( category1Merged.getSubCategories().contains( subCategory1 ) );
assertTrue( category1Merged.getSubCategories().contains( subCategory2 ) );
tx.commit();
s.close();
s = openSession();
tx = s.beginTransaction();
category1 = (Category) s.get( Category.class, category1.getId() );
assertEquals( 2, category1.getSubCategories().size() );
assertTrue( category1.getSubCategories().contains( subCategory1 ) );
assertTrue( category1.getSubCategories().contains( subCategory2 ) );
tx.commit();
s.close();
cleanup();
}
@SuppressWarnings( {"unchecked"})
private void cleanup() {
Session s = openSession();
s.beginTransaction();
s.createQuery( "delete from SubItem" ).executeUpdate();
for ( Hoarder hoarder : (List<Hoarder>) s.createQuery( "from Hoarder" ).list() ) {
hoarder.getItems().clear();
s.delete( hoarder );
}
for ( Category category : (List<Category>) s.createQuery( "from Category" ).list() ) {
if ( category.getExampleItem() != null ) {
category.setExampleItem( null );
s.delete( category );
}
}
for ( Item item : (List<Item>) s.createQuery( "from Item" ).list() ) {
item.setCategory( null );
s.delete( item );
}
s.createQuery( "delete from Item" ).executeUpdate();
s.getTransaction().commit();
s.close();
}
}

View File

@ -745,11 +745,11 @@ public class MergeTest extends AbstractOperationTestCase {
Employer jboss = new Employer(); Employer jboss = new Employer();
Employee gavin = new Employee(); Employee gavin = new Employee();
jboss.setEmployees( new ArrayList() ); jboss.setEmployees( new ArrayList() );
jboss.getEmployees().add(gavin); jboss.getEmployees().add( gavin );
s.merge(jboss); s.merge( jboss );
s.flush(); s.flush();
jboss = (Employer) s.createQuery("from Employer e join fetch e.employees").uniqueResult(); jboss = (Employer) s.createQuery("from Employer e join fetch e.employees").uniqueResult();
assertTrue( Hibernate.isInitialized( jboss.getEmployees() ) ); assertTrue( Hibernate.isInitialized( jboss.getEmployees() ) );
assertEquals( 1, jboss.getEmployees().size() ); assertEquals( 1, jboss.getEmployees().size() );
s.clear(); s.clear();
s.merge( jboss.getEmployees().iterator().next() ); s.merge( jboss.getEmployees().iterator().next() );

View File

@ -0,0 +1,79 @@
/*
* 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.test.ops;
/**
* @author Gail Badner
*/
public class SubCategory {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
SubCategory item = (SubCategory) o;
if ( !name.equals( item.name ) ) {
return false;
}
return true;
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public String toString() {
return "SubItem{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

View File

@ -0,0 +1,79 @@
/*
* 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.test.ops;
/**
* @author Gail Badner
*/
public class SubItem {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if ( this == o ) {
return true;
}
if ( o == null || getClass() != o.getClass() ) {
return false;
}
SubItem item = (SubItem) o;
if ( !name.equals( item.name ) ) {
return false;
}
return true;
}
@Override
public int hashCode() {
return name.hashCode();
}
@Override
public String toString() {
return "SubItem{" +
"id=" + id +
", name='" + name + '\'' +
'}';
}
}

View File

@ -16,4 +16,7 @@ log4j.logger.org.hibernate.hql.internal.ast=debug
log4j.logger.org.hibernate.sql.ordering.antlr=debug log4j.logger.org.hibernate.sql.ordering.antlr=debug
log4j.logger.org.hibernate.engine.internal.LoggingSessionEventListener=info log4j.logger.org.hibernate.engine.internal.LoggingSessionEventListener=info
### enable the following line to log multiple entity representations being merged for a persistent entity.
#log4j.logger.org.hibernate.event.internal.DefaultEntityCopyObserver=debug

View File

@ -0,0 +1,42 @@
/*
* 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.jpa.event.internal.core;
import org.hibernate.event.internal.EntityCopyNotAllowedObserver;
import org.hibernate.event.internal.EntityCopyObserver;
/**
* Overrides {@link JpaMergeEventListener} to disallow merging multiple representations
* of the same persistent entity.
*
* @author Gail Badner
*/
public class JpaNoEntityCopiesMergeEventListener extends JpaMergeEventListener {
@Override
protected EntityCopyObserver createDetachedEntityCopyObserver() {
return new EntityCopyNotAllowedObserver();
}
}

View File

@ -37,3 +37,6 @@ log4j.logger.org.hibernate.tool.hbm2ddl=debug
### enable the following line if you want to track down connection ### ### enable the following line if you want to track down connection ###
### 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.
#log4j.logger.org.hibernate.event.internal.DefaultEntityCopyObserver=debug