diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java index 1aeef9404c..fd76992997 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java @@ -1322,6 +1322,10 @@ public class StatefulPersistenceContext implements PersistenceContext { collectionPersister, 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, mergeMap.get( proxy ) ); + LOG.debugf( + "Detached proxy being merged has a collection that [%s] the managed child.", + (found ? "contains" : "does not contain") + ); if ( !found ) { found = isFoundInParent( propertyName, @@ -1356,6 +1364,10 @@ public class StatefulPersistenceContext implements PersistenceContext { collectionPersister, 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 ) { return proxy.getHibernateLazyInitializer().getIdentifier(); @@ -1402,10 +1414,14 @@ public class StatefulPersistenceContext implements PersistenceContext { Object index = getIndexInParent(property, childEntity, persister, cp, parent); if (index==null && mergeMap!=null) { - Object unmergedInstance = mergeMap.get(parent); - Object unmergedChild = mergeMap.get(childEntity); - if ( unmergedInstance!=null && unmergedChild!=null ) { - index = getIndexInParent(property, unmergedChild, persister, cp, unmergedInstance); + final Object unMergedInstance = mergeMap.get( parent ); + final Object unMergedChild = mergeMap.get( childEntity ); + if ( unMergedInstance != null && unMergedChild != null ) { + 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) { @@ -1421,15 +1437,18 @@ public class StatefulPersistenceContext implements PersistenceContext { for ( Entry me : reentrantSafeEntityEntries() ) { EntityEntry ee = me.getValue(); if ( persister.isSubclassEntityName( ee.getEntityName() ) ) { - Object instance = me.getKey(); + final Object instance = me.getKey(); - Object index = getIndexInParent(property, childEntity, persister, cp, instance); - - if (index==null && mergeMap!=null) { - Object unmergedInstance = mergeMap.get(instance); - Object unmergedChild = mergeMap.get(childEntity); - if ( unmergedInstance!=null && unmergedChild!=null ) { - index = getIndexInParent(property, unmergedChild, persister, cp, unmergedInstance); + Object index = getIndexInParent( property, childEntity, persister, cp, instance ); + if ( index==null && mergeMap!=null ) { + final Object unMergedInstance = mergeMap.get( instance ); + final Object unMergedChild = mergeMap.get( childEntity ); + if ( unMergedInstance != null && unMergedChild!=null ) { + 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" ) + ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEntityCopyObserver.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEntityCopyObserver.java new file mode 100644 index 0000000000..4d18bdeebf --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEntityCopyObserver.java @@ -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> 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 detachedEntitiesForManaged = null; + if ( managedToMergeEntitiesXref == null ) { + // This is the first time multiple representations have been found; + // instantiate managedToMergeEntitiesXref. + managedToMergeEntitiesXref = new IdentityHashMap>(); + } + else { + // Get any existing representations that have already been found. + detachedEntitiesForManaged = managedToMergeEntitiesXref.get( managedEntity ); + } + if ( detachedEntitiesForManaged == null ) { + // There were no existing representations; instantiate detachedEntitiesForManaged + detachedEntitiesForManaged = new IdentitySet(); + managedToMergeEntitiesXref.put( managedEntity, detachedEntitiesForManaged ); + } + // Now add the detached representation for the managed entity. + detachedEntitiesForManaged.add( mergeEntity1 ); + detachedEntitiesForManaged.add( mergeEntity2 ); + } + } + + 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> 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 + "]"; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java index c0ee70f16c..5f8fa47d02 100755 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java @@ -1,7 +1,7 @@ /* * 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 * statements applied by the authors. All third-party contributions are * distributed under license by Red Hat Inc. @@ -61,8 +61,12 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme DefaultMergeEventListener.class.getName()); @Override - protected Map getMergeMap(Object anything) { - return ( ( EventCache ) anything ).invertMap(); + protected Map getMergeMap(Object anything) { + return ( (MergeContext) anything ).invertMap(); + } + + protected EntityCopyObserver createDetachedEntityCopyObserver() { + return new DefaultEntityCopyObserver(); } /** @@ -72,10 +76,16 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme * @throws HibernateException */ public void onMerge(MergeEvent event) throws HibernateException { - EventCache copyCache = new EventCache( event.getSession() ); - onMerge( event, copyCache ); - copyCache.clear(); - copyCache = null; + final EntityCopyObserver entityCopyObserver = createDetachedEntityCopyObserver(); + final MergeContext mergeContext = new MergeContext( event.getSession(), entityCopyObserver ); + try { + 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 { - final EventCache copyCache = ( EventCache ) copiedAlready; + final MergeContext copyCache = (MergeContext) copiedAlready; final EventSource source = event.getSession(); final Object original = event.getOriginal(); @@ -178,7 +188,7 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme final EventSource source = event.getSession(); 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); copyValues(persister, entity, entity, source, copyCache); @@ -203,7 +213,7 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme persister.setIdentifier( copyCache.get( entity ), id, source ); } 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 ); @@ -282,7 +292,7 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme entityIsTransient(event, copyCache); } else { - ( ( EventCache ) copyCache ).put( entity, result, true ); //before cascade! + ( (MergeContext) copyCache ).put( entity, result, true ); //before cascade! final Object target = source.getPersistenceContext().unproxy(result); if ( target == entity ) { diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyNotAllowedObserver.java b/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyNotAllowedObserver.java new file mode 100644 index 0000000000..c647448e5c --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyNotAllowedObserver.java @@ -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 + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyObserver.java b/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyObserver.java new file mode 100644 index 0000000000..d32b01ec80 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/EntityCopyObserver.java @@ -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(); +} diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/EventCache.java b/hibernate-core/src/main/java/org/hibernate/event/internal/EventCache.java deleted file mode 100644 index 7c4bc9a26a..0000000000 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/EventCache.java +++ /dev/null @@ -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. - *

- * There are some restriction; - *

    - *
  • the same value cannot be associated with more than one key
  • - *
  • Methods that return collections (e.g., {@link #keySet()}, - * {@link #values()}, {@link #entrySet()}) return an - * unnmodifiable view of the collection.
  • - *
- *

- * 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:

- * {@link EventCache#put(Object entity, Object copy, boolean isOperatedOn)} - *

- * 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 entityToCopyMap = new IdentityHashMap(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 copyToEntityMap = new IdentityHashMap( 10 ); - // maintains the inverse of the entityToCopyMap for performance reasons. - - private Map entityToOperatedOnFlagMap = new IdentityHashMap( 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 + "]"; - } -} \ No newline at end of file diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/MergeContext.java b/hibernate-core/src/main/java/org/hibernate/event/internal/MergeContext.java new file mode 100644 index 0000000000..c3e9657057 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/MergeContext.java @@ -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. + *

    + *
  • Methods that return collections (e.g., {@link #keySet()}, + * {@link #values()}, {@link #entrySet()}) return an + * unnmodifiable view of the collection;
  • + *
  • 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 + * mergeEntity, then managedEntity must be the + * same as what is already associated with mergeEntity in this + * MergeContext. + *
  • + *
  • If {@link #putAll(Map map)} is executed, the previous restriction + * applies to each entry in the Map;
  • + *
  • The {@link #remove(Object)} operation is not supported; + * The only way to remove data from a MergeContext is by calling + * {@link #clear()};
  • + *
  • the Map returned by {@link #invertMap()} will only contain the + * managed-to-merge entity cross-reference to its "newest" + * (most recently added) merge entity.
  • + *
+ *

+ * 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.

+ * {@link MergeContext#put(Object mergeEntity, Object managedEntity, boolean isOperatedOn)} + *

+ * 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 mergeToManagedEntityXref = new IdentityHashMap(10); + // key is an entity to be merged; + // value is the associated managed entity (result) in the persistence context. + + private Map managedToMergeEntityXref = new IdentityHashMap( 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 mergeEntityToOperatedOnFlagMap = new IdentityHashMap( 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 mergeEntity when this + * method is called, then managedEntity must be the same as what is already associated + * with mergeEntity. + *

+ * This method assumes that the merge process is not yet operating on mergeEntity. + * Later when mergeEntity enters the merge process, {@link #setOperatedOn(Object, boolean)} + * should be called. + *

+ * @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 managedEntity is not the same as the previous + * managed entity associated with merge entity + * @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 mergeEntity when this + * method is called, then managedEntity must be the same as what is already associated + * with mergeEntity. + * + * @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 managedEntity is not the same as the previous + * managed entity associated with mergeEntity + * @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 map 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 map was already in this MergeContext + * but associated value in map 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 + "]"; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/NoEntityCopiesMergeEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/NoEntityCopiesMergeEventListener.java new file mode 100644 index 0000000000..4459df95c3 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/NoEntityCopiesMergeEventListener.java @@ -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(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/event/internal/EventCacheTest.java b/hibernate-core/src/test/java/org/hibernate/event/internal/EventCacheTest.java deleted file mode 100644 index ff3576e59e..0000000000 --- a/hibernate-core/src/test/java/org/hibernate/event/internal/EventCacheTest.java +++ /dev/null @@ -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 input = new HashMap(); - 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; - } - } -} diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/Category.java b/hibernate-core/src/test/java/org/hibernate/test/ops/Category.java new file mode 100644 index 0000000000..0fcdd363d0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/Category.java @@ -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 subCategories = new HashSet(); + + 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 getSubCategories() { + return subCategories; + } + + public void setSubCategories(Set 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 + + '}'; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/Hoarder.hbm.xml b/hibernate-core/src/test/java/org/hibernate/test/ops/Hoarder.hbm.xml new file mode 100644 index 0000000000..f91a6d1bee --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/Hoarder.hbm.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/Hoarder.java b/hibernate-core/src/test/java/org/hibernate/test/ops/Hoarder.java new file mode 100644 index 0000000000..3ad62338a5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/Hoarder.java @@ -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 items = new HashSet(); + + 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 getItems() { + return items; + } + + public void setItems(Set items) { + this.items = items; + } + + @Override + public String toString() { + return "Hoarder{" + + "id=" + id + + ", name='" + name + '\'' + + ", favoriteItem=" + favoriteItem + + '}'; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/HoarderOrphanDelete.hbm.xml b/hibernate-core/src/test/java/org/hibernate/test/ops/HoarderOrphanDelete.hbm.xml new file mode 100644 index 0000000000..7bbb9ea0cf --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/HoarderOrphanDelete.hbm.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/Item.java b/hibernate-core/src/test/java/org/hibernate/test/ops/Item.java new file mode 100644 index 0000000000..1d7638e695 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/Item.java @@ -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 subItemsBackref = new ArrayList(); + private Set colors = new HashSet(); + + 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 getColors() { + return colors; + } + + public void setColors(Set colors) { + this.colors = colors; + } + + public List getSubItemsBackref() { + return subItemsBackref; + } + + public void setSubItemsBackref(List 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 + + '}'; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsNotAllowedTest.java b/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsNotAllowedTest.java new file mode 100644 index 0000000000..fbef728568 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsNotAllowedTest.java @@ -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) s.createQuery( "from Hoarder" ).list() ) { + hoarder.getItems().clear(); + s.delete( hoarder ); + } + + for ( Category category : (List) s.createQuery( "from Category" ).list() ) { + if ( category.getExampleItem() != null ) { + category.setExampleItem( null ); + s.delete( category ); + } + } + + for ( Item item : (List) s.createQuery( "from Item" ).list() ) { + item.setCategory( null ); + s.delete( item ); + } + + s.createQuery( "delete from Item" ).executeUpdate(); + + s.getTransaction().commit(); + s.close(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsOrphanDeleteTest.java b/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsOrphanDeleteTest.java new file mode 100644 index 0000000000..d2d6163aa3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsOrphanDeleteTest.java @@ -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) s.createQuery( "from Hoarder" ).list() ) { + hoarder.getItems().clear(); + s.delete( hoarder ); + } + + for ( Category category : (List) s.createQuery( "from Category" ).list() ) { + if ( category.getExampleItem() != null ) { + category.setExampleItem( null ); + s.delete( category ); + } + } + + for ( Item item : (List) s.createQuery( "from Item" ).list() ) { + item.setCategory( null ); + s.delete( item ); + } + + s.createQuery( "delete from Item" ).executeUpdate(); + + s.getTransaction().commit(); + s.close(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsTest.java b/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsTest.java new file mode 100644 index 0000000000..ab2b0597f4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/MergeMultipleEntityRepresentationsTest.java @@ -0,0 +1,1146 @@ +/* + * 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.StaleObjectStateException; +import org.hibernate.Transaction; +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.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +/** + * Tests merging multiple detached representations of the same entity. + * + * @author Gail Badner + */ +public class MergeMultipleEntityRepresentationsTest extends BaseCoreFunctionalTestCase { + + public String[] getMappings() { + return new String[] { + "ops/Hoarder.hbm.xml" + }; + } + + @Test + public void testNestedDiffBasicProperty() { + 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(); + + // change basic property of nested entity + item1_1.setName( "item1_1 name" ); + + // change the nested Item to be the copy with the new name + item1.getCategory().setExampleItem( item1_1 ); + + s = openSession(); + tx = s.beginTransaction(); + Item item1Merged = (Item) s.merge( item1 ); + // the name from the top level item will win. + assertEquals( item1.getName(), item1Merged.getName() ); + tx.commit(); + s.close(); + + s = openSession(); + tx = s.beginTransaction(); + Item item1Get = (Item) s.get( Item.class, item1.getId() ); + assertEquals( item1.getName(), item1Get.getName() ); + tx.commit(); + s.close(); + + cleanup(); + } + + @Test + public void testNestedManyToOneChangedToNull() { + 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(); + + // change many-to-one in nested entity to null. + item1_1.setCategory( null ); + item1.getCategory().setExampleItem( item1_1 ); + + s = openSession(); + tx = s.beginTransaction(); + Item item1Merged = (Item) s.merge( item1 ); + // the many-to-one from the top level item will win. + assertEquals( category.getName(), item1Merged.getCategory().getName() ); + assertSame( item1Merged, item1Merged.getCategory().getExampleItem() ); + tx.commit(); + s.close(); + + s = openSession(); + tx = s.beginTransaction(); + item1 = (Item) s.get( Item.class, item1.getId() ); + assertEquals( category.getName(), item1.getCategory().getName() ); + assertSame( item1, item1.getCategory().getExampleItem() ); + tx.commit(); + s.close(); + + cleanup(); + } + + @Test + public void testNestedManyToOneChangedToNewEntity() { + 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(); + + // change many-to-one in nested entity to a new (transient) value + Category categoryNew = new Category(); + categoryNew.setName( "new category" ); + item1_1.setCategory( categoryNew ); + item1.getCategory().setExampleItem( item1_1 ); + + s = openSession(); + tx = s.beginTransaction(); + Item item1Merged = (Item) s.merge( item1 ); + // the many-to-one from the top level item will win. + assertEquals( category.getName(), item1Merged.getCategory().getName() ); + assertSame( item1Merged, item1Merged.getCategory().getExampleItem() ); + tx.commit(); + s.close(); + + s = openSession(); + tx = s.beginTransaction(); + item1 = (Item) s.get( Item.class, item1.getId() ); + assertEquals( category.getName(), item1.getCategory().getName() ); + assertSame( item1, item1.getCategory().getExampleItem() ); + // make sure new category got persisted + Category categoryQueried = (Category) s.createQuery( "from Category c where c.name='new category'" ).uniqueResult(); + assertNotNull( categoryQueried ); + tx.commit(); + s.close(); + + cleanup(); + } + + @Test + public void testTopLevelManyToOneChangedToNewEntity() { + 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(); + + // change many-to-one in top level to be a new (transient) + Category categoryNewer = new Category(); + categoryNewer.setName( "newer category" ); + item1.setCategory( categoryNewer ); + + // put the other representation in categoryNewer + categoryNewer.setExampleItem( item1_1 ); + + s = openSession(); + tx = s.beginTransaction(); + Item item1Merged = (Item) s.merge( item1 ); + // the many-to-one from the top level item will win. + assertEquals( categoryNewer.getName(), item1Merged.getCategory().getName() ); + assertSame( item1Merged, item1Merged.getCategory().getExampleItem() ); + tx.commit(); + s.close(); + + s = openSession(); + tx = s.beginTransaction(); + item1 = (Item) s.get( Item.class, item1.getId() ); + assertEquals( categoryNewer.getName(), item1.getCategory().getName() ); + assertSame( item1, item1.getCategory().getExampleItem() ); + // make sure original category is still there + Category categoryQueried = (Category) s.createQuery( "from Category c where c.name='category'" ).uniqueResult(); + assertNotNull( categoryQueried ); + // make sure original category has the same item. + assertSame( item1, categoryQueried.getExampleItem() ); + // set exampleItem to null to avoid constraint violation on cleanup. + categoryQueried.setExampleItem( null ); + tx.commit(); + 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 + s.merge( item1Merged ); + assertEquals( category.getName(), item1Merged.getCategory().getName() ); + assertSame( item1Merged, item1Merged.getCategory().getExampleItem() ); + tx.commit(); + s.close(); + + s = openSession(); + tx = s.beginTransaction(); + item1 = (Item) s.get( Item.class, item1.getId() ); + assertEquals( category.getName(), item1.getCategory().getName() ); + assertSame( item1, item1.getCategory().getExampleItem() ); + tx.commit(); + s.close(); + + cleanup(); + } + + @Test + public void testNestedValueCollectionWithChangedElements() { + Item item1 = new Item(); + item1.setName( "item1 name" ); + Category category = new Category(); + category.setName( "category" ); + item1.getColors().add( "red" ); + + 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() ); + Hibernate.initialize( item1_1.getColors() ); + tx.commit(); + s.close(); + + // add an element to collection in nested entity + item1_1.getColors().add( "blue" ); + item1.getCategory().setExampleItem( item1_1 ); + + s = openSession(); + tx = s.beginTransaction(); + Item item1Merged = (Item) s.merge( item1 ); + // the collection from the top level item will win. + assertEquals( 1, item1Merged.getColors().size() ); + assertEquals( "red", item1Merged.getColors().iterator().next() ); + tx.commit(); + s.close(); + + s = openSession(); + tx = s.beginTransaction(); + item1 = (Item) s.get( Item.class, item1.getId() ); + assertEquals( 1, item1.getColors().size() ); + assertEquals( "red", item1.getColors().iterator().next() ); + Hibernate.initialize( item1.getCategory() ); + tx.commit(); + s.close(); + + // get another representation of item1 + s = openSession(); + tx = s.beginTransaction(); + item1_1 = (Item) s.get( Item.class, item1.getId() ); + Hibernate.initialize( item1_1.getColors() ); + tx.commit(); + s.close(); + + // remove the existing elements from collection in nested entity + item1_1.getColors().clear(); + item1.getCategory().setExampleItem( item1_1 ); + + s = openSession(); + tx = s.beginTransaction(); + item1Merged = (Item) s.merge( item1 ); + // the collection from the top level item will win. + assertEquals( 1, item1Merged.getColors().size() ); + assertEquals( "red", item1Merged.getColors().iterator().next() ); + tx.commit(); + s.close(); + + s = openSession(); + tx = s.beginTransaction(); + item1 = (Item) s.get( Item.class, item1.getId() ); + assertEquals( 1, item1.getColors().size() ); + assertEquals( "red", item1.getColors().iterator().next() ); + tx.commit(); + s.close(); + + cleanup(); + } + + @Test + public void testTopValueCollectionWithChangedElements() { + Item item1 = new Item(); + item1.setName( "item1 name" ); + Category category = new Category(); + category.setName( "category" ); + item1.getColors().add( "red" ); + + 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(); + + // add an element to collection in nested entity + item1.getColors().add( "blue" ); + item1.getCategory().setExampleItem( item1_1 ); + + s = openSession(); + tx = s.beginTransaction(); + Item item1Merged = (Item) s.merge( item1 ); + // the collection from the top level item will win. + assertEquals( 2, item1Merged.getColors().size() ); + assertTrue( item1Merged.getColors().contains( "red" ) ); + assertTrue( item1Merged.getColors().contains( "blue" ) ); + tx.commit(); + s.close(); + + s = openSession(); + tx = s.beginTransaction(); + item1 = (Item) s.get( Item.class, item1.getId() ); + assertEquals( 2, item1.getColors().size() ); + assertTrue( item1.getColors().contains( "red" ) ); + assertTrue( item1.getColors().contains( "blue" ) ); + Hibernate.initialize( item1.getCategory() ); + tx.commit(); + s.close(); + + // get another representation of item1 + s = openSession(); + tx = s.beginTransaction(); + item1_1 = (Item) s.get( Item.class, item1.getId() ); + tx.commit(); + s.close(); + + // remove the existing elements from collection in nested entity + item1.getColors().clear(); + item1.getCategory().setExampleItem( item1_1 ); + + s = openSession(); + tx = s.beginTransaction(); + item1Merged = (Item) s.merge( item1 ); + // the collection from the top level item will win. + assertTrue( item1Merged.getColors().isEmpty() ); + tx.commit(); + s.close(); + + s = openSession(); + tx = s.beginTransaction(); + item1 = (Item) s.get( Item.class, item1.getId() ); + assertTrue( item1.getColors().isEmpty() ); + tx.commit(); + s.close(); + + cleanup(); + } + + @Test + public void testCascadeFromTransientToNonDirtyRepresentations() { + + Item item1 = new Item(); + item1.setName( "item1" ); + + Session s = openSession(); + Transaction tx = session.beginTransaction(); + s.persist( item1 ); + 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 ) ); + + // Create a transient entity that references both representations. + Hoarder hoarder = new Hoarder(); + hoarder.setName( "joe" ); + hoarder.getItems().add( item1 ); + hoarder.setFavoriteItem( item1_1 ); + + s = openSession(); + tx = s.beginTransaction(); + hoarder = (Hoarder) s.merge( hoarder ); + assertEquals( 1, hoarder.getItems().size() ); + assertSame( hoarder.getFavoriteItem(), hoarder.getItems().iterator().next() ); + assertEquals( item1.getId(), hoarder.getFavoriteItem().getId() ); + assertEquals( item1.getCategory(), hoarder.getFavoriteItem().getCategory() ); + tx.commit(); + s.close(); + + s = openSession(); + tx = s.beginTransaction(); + hoarder = (Hoarder) s.merge( hoarder ); + assertEquals( 1, hoarder.getItems().size() ); + assertSame( hoarder.getFavoriteItem(), hoarder.getItems().iterator().next() ); + assertEquals( item1.getId(), hoarder.getFavoriteItem().getId() ); + assertEquals( item1.getCategory(), hoarder.getFavoriteItem().getCategory() ); + tx.commit(); + s.close(); + + cleanup(); + } + + @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(); + hoarder = (Hoarder) s.merge( hoarder ); + assertEquals( 1, hoarder.getItems().size() ); + assertSame( hoarder.getFavoriteItem(), hoarder.getItems().iterator().next() ); + assertEquals( item1.getId(), hoarder.getFavoriteItem().getId() ); + assertEquals( item1.getCategory(), hoarder.getFavoriteItem().getCategory() ); + tx.commit(); + s.close(); + + s = openSession(); + tx = s.beginTransaction(); + hoarder = (Hoarder) s.merge( hoarder ); + assertEquals( 1, hoarder.getItems().size() ); + assertSame( hoarder.getFavoriteItem(), hoarder.getItems().iterator().next() ); + assertEquals( item1.getId(), hoarder.getFavoriteItem().getId() ); + assertEquals( item1.getCategory(), hoarder.getFavoriteItem().getCategory() ); + tx.commit(); + s.close(); + + cleanup(); + } + + @Test + public void testTopLevelEntityNewerThanNested() { + Item item = new Item(); + item.setName( "item" ); + + Category category = new Category(); + category.setName( "category" ); + + Session s = openSession(); + Transaction tx = s.beginTransaction(); + s.persist( item ); + s.persist( category ); + tx.commit(); + s.close(); + + // Get the Category from a different session. + s = openSession(); + tx = s.beginTransaction(); + Category category1_2 = (Category) s.get( Category.class, category.getId() ); + tx.commit(); + s.close(); + + // Get and update the same Category. + s = openSession(); + tx = s.beginTransaction(); + Category category1_1 = (Category) s.get( Category.class, category.getId() ); + category1_1.setName( "new name" ); + tx.commit(); + s.close(); + + assertTrue( category1_2.getVersion() < category1_1.getVersion() ); + + category1_1.setExampleItem( item ); + item.setCategory( category1_2 ); + + s = openSession(); + tx = s.beginTransaction(); + try { + // representation merged at top level is newer than nested representation. + category1_1 = (Category) s.merge( category1_1 ); + fail( "should have failed because one representation is an older version." ); + } + catch( StaleObjectStateException ex ) { + // expected + } + finally { + tx.rollback(); + s.close(); + } + + cleanup(); + } + + @Test + public void testNestedEntityNewerThanTopLevel() { + Item item = new Item(); + item.setName( "item" ); + + Category category = new Category(); + category.setName( "category" ); + + Session s = openSession(); + Transaction tx = s.beginTransaction(); + s.persist( item ); + s.persist( category ); + tx.commit(); + s.close(); + + // Get category1_1 from a different session. + s = openSession(); + Category category1_1 = (Category) s.get( Category.class, category.getId() ); + s.close(); + + // Get and update category1_2 to increment its version. + s = openSession(); + tx = s.beginTransaction(); + Category category1_2 = (Category) s.get( Category.class, category.getId() ); + category1_2.setName( "new name" ); + tx.commit(); + s.close(); + + assertTrue( category1_2.getVersion() > category1_1.getVersion() ); + + category1_1.setExampleItem( item ); + item.setCategory( category1_2 ); + + s = openSession(); + tx = s.beginTransaction(); + try { + // nested representation is newer than top lever representation. + category1_1 = (Category) s.merge( category1_1 ); + fail( "should have failed because one representation is an older version." ); + } + catch( StaleObjectStateException ex ) { + // expected + } + finally { + tx.rollback(); + s.close(); + } + + cleanup(); + } + + @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 + //@FailureExpected( 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 ); + + 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 (effectively removing the new SubItem from + // the collection). Because delete-orpban is not enabled, the new SubItem should not be + // deleted; an exception should have been thrown because the new SubItem is no longer + // in a collection and the collection key is non-nullable. + // + // Because cascade includes "delete-orphan", that new SubItem should have been + // deleted, but due to HHH-9171 it was not deleted. Because the collection key + // is non-nullable, SubItem still has a backref to the same collection owner. + // 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 due to HHH-9171 because in that new SubItem (that should + // have been deleted) from the previous transaction gets loaded into the collection + // because it still has the collection key set. + 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() ); + // cascade does not include delete-orphan, so subItem1 should still be persistent. + assertNotNull( 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. + 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 ) ); + // cascade does not include delete-orphan, so subCategory1 should still be persistent. + subCategory1 = (SubCategory) s.get( SubCategory.class, subCategory1.getId() ); + assertNotNull( 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) s.createQuery( "from Hoarder" ).list() ) { + hoarder.getItems().clear(); + s.delete( hoarder ); + } + + for ( Category category : (List) s.createQuery( "from Category" ).list() ) { + if ( category.getExampleItem() != null ) { + category.setExampleItem( null ); + s.delete( category ); + } + } + + for ( Item item : (List) s.createQuery( "from Item" ).list() ) { + item.setCategory( null ); + s.delete( item ); + } + + s.createQuery( "delete from Item" ).executeUpdate(); + + s.getTransaction().commit(); + s.close(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/MergeTest.java b/hibernate-core/src/test/java/org/hibernate/test/ops/MergeTest.java index f3a01eb033..6b2b2d7804 100755 --- a/hibernate-core/src/test/java/org/hibernate/test/ops/MergeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/MergeTest.java @@ -745,11 +745,11 @@ public class MergeTest extends AbstractOperationTestCase { Employer jboss = new Employer(); Employee gavin = new Employee(); jboss.setEmployees( new ArrayList() ); - jboss.getEmployees().add(gavin); - s.merge(jboss); + jboss.getEmployees().add( gavin ); + s.merge( jboss ); s.flush(); 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() ); s.clear(); s.merge( jboss.getEmployees().iterator().next() ); diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/SubCategory.java b/hibernate-core/src/test/java/org/hibernate/test/ops/SubCategory.java new file mode 100644 index 0000000000..ceab761184 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/SubCategory.java @@ -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 + '\'' + + '}'; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/test/ops/SubItem.java b/hibernate-core/src/test/java/org/hibernate/test/ops/SubItem.java new file mode 100644 index 0000000000..ce81a53cb8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/test/ops/SubItem.java @@ -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 + '\'' + + '}'; + } +} diff --git a/hibernate-core/src/test/resources/log4j.properties b/hibernate-core/src/test/resources/log4j.properties index 59b6c86c83..7bab61e9af 100644 --- a/hibernate-core/src/test/resources/log4j.properties +++ b/hibernate-core/src/test/resources/log4j.properties @@ -16,4 +16,7 @@ log4j.logger.org.hibernate.hql.internal.ast=debug log4j.logger.org.hibernate.sql.ordering.antlr=debug -log4j.logger.org.hibernate.engine.internal.LoggingSessionEventListener=info \ No newline at end of file +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 diff --git a/hibernate-entitymanager/src/main/java/org/hibernate/jpa/event/internal/core/JpaNoEntityCopiesMergeEventListener.java b/hibernate-entitymanager/src/main/java/org/hibernate/jpa/event/internal/core/JpaNoEntityCopiesMergeEventListener.java new file mode 100644 index 0000000000..50885c03a3 --- /dev/null +++ b/hibernate-entitymanager/src/main/java/org/hibernate/jpa/event/internal/core/JpaNoEntityCopiesMergeEventListener.java @@ -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(); + } + +} diff --git a/hibernate-entitymanager/src/test/resources/log4j.properties b/hibernate-entitymanager/src/test/resources/log4j.properties index 29d8ee3281..50249d4a60 100755 --- a/hibernate-entitymanager/src/test/resources/log4j.properties +++ b/hibernate-entitymanager/src/test/resources/log4j.properties @@ -37,3 +37,6 @@ log4j.logger.org.hibernate.tool.hbm2ddl=debug ### enable the following line if you want to track down connection ### ### leakages when using DriverManagerConnectionProvider ### #log4j.logger.org.hibernate.connection.DriverManagerConnectionProvider=trace + +### enable the following line to log multiple entity representations being merged for a persistent entity. +#log4j.logger.org.hibernate.event.internal.DefaultEntityCopyObserver=debug