[hibernate-commits] Hibernate SVN: r16568 - in core/branches/Branch_3_3: testsuite/src/test/java/org/hibernate/test/cascade and 2 other directories.

hibernate-commits at lists.jboss.org hibernate-commits at lists.jboss.org
Thu May 14 15:57:00 EDT 2009


Author: gbadner
Date: 2009-05-14 15:57:00 -0400 (Thu, 14 May 2009)
New Revision: 16568

Added:
   core/branches/Branch_3_3/core/src/main/java/org/hibernate/event/def/CopyCache.java
   core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/
   core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParent.hbm.xml
   core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParentTest.java
   core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascade.hbm.xml
   core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeTest.java
   core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Node.java
   core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Route.java
   core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Tour.java
   core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Transport.java
   core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Vehicle.java
Modified:
   core/branches/Branch_3_3/core/src/main/java/org/hibernate/event/def/DefaultMergeEventListener.java
   core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/A.java
   core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/G.java
   core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/H.java
   core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascadeTest.java
   core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/ops/MergeTest.java
Log:
HHH-3810 : Transient entities can be inserted twice on merge


Added: core/branches/Branch_3_3/core/src/main/java/org/hibernate/event/def/CopyCache.java
===================================================================
--- core/branches/Branch_3_3/core/src/main/java/org/hibernate/event/def/CopyCache.java	                        (rev 0)
+++ core/branches/Branch_3_3/core/src/main/java/org/hibernate/event/def/CopyCache.java	2009-05-14 19:57:00 UTC (rev 16568)
@@ -0,0 +1,244 @@
+//$Id: $
+/*
+ * 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.def;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.Iterator;
+import java.util.Collection;
+
+import org.hibernate.util.IdentityMap;
+import org.hibernate.AssertionFailure;
+
+/**
+ * CopyCache is intended to be the Map implementation used by
+ * {@link DefaultMergeEventListener} to keep track of entities and their copies
+ * being merged into the session. This implementation also tracks whether a
+ * an entity in the CopyCache is included in the merge. This allows a
+ * an entity and its copy to be added to a CopyCache before merge has cascaded
+ * to that entity.
+ *
+ * @author Gail Badner
+ */
+class CopyCache implements Map {
+	private Map entityToCopyMap = IdentityMap.instantiate(10);
+		// key is an entity involved with the merge;
+		// value can be either a copy of the entity or the entity itself
+
+	private Map entityToIncludeInMergeFlagMap = IdentityMap.instantiate(10);
+	    // key is an entity involved with the merge;
+	    // value is a flag indicating if the entity is included in the merge
+
+	/**
+	 * Clears the CopyCache.
+	 */
+	public void clear() {
+		entityToCopyMap.clear();
+		entityToIncludeInMergeFlagMap.clear();
+	}
+
+	/**
+	 * Returns true if this CopyCache contains a mapping for the specified entity.
+	 * @param entity must be non-null
+	 * @return true if this CopyCache 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 CopyCache maps one or more entities to the specified copy.
+	 * @param copy must be non-null
+	 * @return true if this CopyCache maps one or more entities 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 entityToCopyMap.containsValue( copy );
+	}
+
+	/**
+	 * Returns a set view of the entity-to-copy mappings contained in this CopyCache.
+	 * @return set view of the entity-to-copy mappings contained in this CopyCache
+	 */
+	public Set entrySet() {
+		return entityToCopyMap.entrySet();
+	}
+
+	/**
+	 * Returns the copy to which this CopyCache maps the specified entity.
+	 * @param entity must be non-null
+	 * @return the copy to which this CopyCache 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 CopyCache contains no entity-copy mappings.
+	 * @return true if this CopyCache contains no entity-copy mappings
+	 */
+	public boolean isEmpty() {
+		return entityToCopyMap.isEmpty();
+	}
+
+	/**
+	 * Returns a set view of the entities contained in this CopyCache
+	 * @return a set view of the entities contained in this CopyCache
+	 */
+	public Set keySet() {
+		return entityToCopyMap.keySet();
+	}
+
+	/**
+	 * Associates the specified entity with the specified copy in this CopyCache;
+	 * @param entity must be non-null
+	 * @param copy must be non- null
+	 * @return previous copy associated with specified entity, or null if
+	 * there was no mapping for entity.
+	 * @throws NullPointerException if entity or copy is null
+	 */
+	public Object put(Object entity, Object copy) {
+		if ( entity == null || copy == null ) {
+			throw new NullPointerException( "null entities and copies are not supported by " + getClass().getName() );
+		}
+		entityToIncludeInMergeFlagMap.put( entity, Boolean.FALSE );
+		return entityToCopyMap.put( entity, copy );
+	}
+
+	/**
+	 * Associates the specified entity with the specified copy in this CopyCache;
+	 * @param entity must be non-null
+	 * @param copy must be non- null
+	 * @param isIncludedInMerge indicates if the entity is included in merge
+	 *
+	 * @return previous copy associated with specified entity, or null if
+	 * there was no mapping for entity.
+	 * @throws NullPointerException if entity or copy is null
+	 */
+	/* package-private */ Object put(Object entity, Object copy, boolean isIncludedInMerge) {
+		if ( entity == null || copy == null ) {
+			throw new NullPointerException( "null entities and copies are not supported by " + getClass().getName() );
+		}
+		entityToIncludeInMergeFlagMap.put( entity, Boolean.valueOf( isIncludedInMerge ) );
+		return entityToCopyMap.put( entity, copy );
+	}
+
+	/**
+	 * Copies all of the mappings from the specified map to this CopyCache
+	 * @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 ( Iterator it=map.entrySet().iterator(); it.hasNext(); ) {
+			Map.Entry entry = ( Map.Entry ) it.next();
+			if ( entry.getKey() == null || entry.getValue() == null ) {
+				throw new NullPointerException( "null entities and copies are not supported by " + getClass().getName() );
+			}
+			entityToCopyMap.put( entry.getKey(), entry.getValue() );
+			entityToIncludeInMergeFlagMap.put( entry.getKey(), Boolean.FALSE );
+		}
+	}
+
+	/**
+	 * Removes the mapping for this entity from this CopyCache 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() );
+		}
+		entityToIncludeInMergeFlagMap.remove( entity );
+		return entityToCopyMap.remove( entity );
+	}
+
+	/**
+	 * Returns the number of entity-copy mappings in this CopyCache
+	 * @return the number of entity-copy mappings in this CopyCache
+	 */
+	public int size() {
+		return entityToCopyMap.size();
+	}
+
+	/**
+	 * Returns a collection view of the entity copies contained in this CopyCache.
+	 * @return a collection view of the entity copies contained in this CopyCache
+	 */
+	public Collection values() {
+		return entityToCopyMap.values();
+	}
+
+	/**
+	 * Returns true if the specified entity is included in the merge.
+	 * @param entity must be non-null
+	 * @return true if the specified entity is included in the merge.
+	 * @throws NullPointerException if entity is null
+	 */
+	public boolean isIncludedInMerge(Object entity) {
+		if ( entity == null ) {
+			throw new NullPointerException( "null entities are not supported by " + getClass().getName() );
+		}
+		return ( ( Boolean ) entityToIncludeInMergeFlagMap.get( entity ) ).booleanValue();
+	}
+
+	/**
+	 * Set flag to indicate if an entity is included in the merge.
+	 * @param entity must be non-null and this CopyCache must contain a mapping for this entity
+	 * @return true if the specified entity is included in the merge
+	 * @throws NullPointerException if entity is null
+	 * @throws AssertionFailure if this CopyCache does not contain a mapping for the specified entity
+	 */
+	/* package-private */ void setIncludedInMerge(Object entity, boolean isIncludedInMerge) {
+		if ( entity == null ) {
+			throw new NullPointerException( "null entities are not supported by " + getClass().getName() );
+		}
+		if ( ! entityToIncludeInMergeFlagMap.containsKey( entity ) ||
+			! entityToCopyMap.containsKey( entity ) ) {
+			throw new AssertionFailure( "called CopyCache.setInMergeProcess() for entity not found in CopyCache" );
+		}
+		entityToIncludeInMergeFlagMap.put( entity, Boolean.valueOf( isIncludedInMerge ) );
+	}
+
+	/**
+	 * Returns the copy-entity mappings
+	 * @return the copy-entity mappings
+	 */
+	public Map getMergeMap() {
+		return IdentityMap.invert( entityToCopyMap );
+	}
+}
\ No newline at end of file

Modified: core/branches/Branch_3_3/core/src/main/java/org/hibernate/event/def/DefaultMergeEventListener.java
===================================================================
--- core/branches/Branch_3_3/core/src/main/java/org/hibernate/event/def/DefaultMergeEventListener.java	2009-05-14 15:40:17 UTC (rev 16567)
+++ core/branches/Branch_3_3/core/src/main/java/org/hibernate/event/def/DefaultMergeEventListener.java	2009-05-14 19:57:00 UTC (rev 16568)
@@ -27,6 +27,8 @@
 import java.io.Serializable;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.Set;
+import java.util.HashSet;
 
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -37,6 +39,7 @@
 import org.hibernate.StaleObjectStateException;
 import org.hibernate.TransientObjectException;
 import org.hibernate.WrongClassException;
+import org.hibernate.PropertyValueException;
 import org.hibernate.engine.Cascade;
 import org.hibernate.engine.CascadingAction;
 import org.hibernate.engine.EntityEntry;
@@ -53,7 +56,7 @@
 import org.hibernate.proxy.LazyInitializer;
 import org.hibernate.type.ForeignKeyDirection;
 import org.hibernate.type.TypeFactory;
-import org.hibernate.util.IdentityMap;
+import org.hibernate.type.Type;
 
 /**
  * Defines the default copy event listener used by hibernate for copying entities
@@ -65,9 +68,9 @@
 	implements MergeEventListener {
 
 	private static final Logger log = LoggerFactory.getLogger(DefaultMergeEventListener.class);
-	
+
 	protected Map getMergeMap(Object anything) {
-		return IdentityMap.invert( (Map) anything );
+		return ( ( CopyCache ) anything ).getMergeMap();
 	}
 
 	/**
@@ -77,36 +80,96 @@
 	 * @throws HibernateException
 	 */
 	public void onMerge(MergeEvent event) throws HibernateException {
-		Map copyCache = IdentityMap.instantiate(10);
+		CopyCache copyCache = new CopyCache();
 		onMerge( event, copyCache );
-		for ( Iterator it=copyCache.values().iterator(); it.hasNext(); ) {
-			Object entity = it.next();
-			if ( entity instanceof HibernateProxy ) {
-				entity = ( (HibernateProxy) entity ).getHibernateLazyInitializer().getImplementation();
+		// TODO: iteratively get transient entities and retry merge until one of the following conditions:
+		//       1) transientCopyCache.size() == 0
+		//       2) transientCopyCache.size() is not decreasing and copyCache.size() is not increasing
+		// TODO: find out if retrying can add entities to copyCache (don't think it can...)
+		// For now, just retry once; throw TransientObjectException if there are still any transient entities
+		Map transientCopyCache = getTransientCopyCache(event, copyCache );
+		if ( transientCopyCache.size() > 0 ) {
+			retryMergeTransientEntities( event, transientCopyCache, copyCache );
+			// find any entities that are still transient after retry
+			transientCopyCache = getTransientCopyCache(event, copyCache );
+			if ( transientCopyCache.size() > 0 ) {
+				Set transientEntityNames = new HashSet();
+				for( Iterator it=transientCopyCache.keySet().iterator(); it.hasNext(); ) {
+					Object transientEntity = it.next();
+					String transientEntityName = event.getSession().guessEntityName( transientEntity );
+					transientEntityNames.add( transientEntityName );
+					log.trace( "transient instance could not be processed by merge: " +
+							transientEntityName + "[" + transientEntity + "]" );
+				}
+				throw new TransientObjectException(
+					"one or more objects is an unsaved transient instance - save transient instance(s) before merging: " +
+					transientEntityNames );
 			}
-			EntityEntry entry = event.getSession().getPersistenceContext().getEntry( entity );
-			if ( entry == null ) {
+		}
+		copyCache.clear();
+		copyCache = null;
+	}
+
+	protected CopyCache getTransientCopyCache(MergeEvent event, CopyCache copyCache) {
+		CopyCache transientCopyCache = new CopyCache();
+		for ( Iterator it=copyCache.entrySet().iterator(); it.hasNext(); ) {
+			Map.Entry mapEntry = ( Map.Entry ) it.next();
+			Object entity = mapEntry.getKey();
+			Object copy = mapEntry.getValue();
+			if ( copy instanceof HibernateProxy ) {
+				copy = ( (HibernateProxy) copy ).getHibernateLazyInitializer().getImplementation();
+			}
+			EntityEntry copyEntry = event.getSession().getPersistenceContext().getEntry( copy );
+			if ( copyEntry == null ) {
+				// entity name will not be available for non-POJO entities
+				// TODO: cache the entity name somewhere so that it is available to this exception
+				log.trace( "transient instance could not be processed by merge: " +
+						event.getSession().guessEntityName( copy ) + "[" + entity + "]" );
 				throw new TransientObjectException(
-						"object references an unsaved transient instance - save the transient instance before merging: " +
-						event.getSession().guessEntityName( entity )
+					"object is an unsaved transient instance - save the transient instance before merging: " +
+						event.getSession().guessEntityName( copy )
 				);
-				// TODO: cache the entity name somewhere so that it is available to this exception
-				// entity name will not be available for non-POJO entities
 			}
-			if ( entry.getStatus() != Status.MANAGED && entry.getStatus() != Status.READ_ONLY ) {
-				throw new AssertionFailure( "Merged entity does not have status set to MANAGED or READ_ONLY; "+entry+" status="+entry.getStatus() );
+			else if ( copyEntry.getStatus() == Status.SAVING ) {
+				transientCopyCache.put( entity, copy, copyCache.isIncludedInMerge( entity ) );
 			}
+			else if ( copyEntry.getStatus() != Status.MANAGED && copyEntry.getStatus() != Status.READ_ONLY ) {
+				throw new AssertionFailure( "Merged entity does not have status set to MANAGED or READ_ONLY; "+copy+" status="+copyEntry.getStatus() );
+			}
 		}
+		return transientCopyCache;
 	}
 
+	protected void retryMergeTransientEntities(MergeEvent event, Map transientCopyCache, CopyCache copyCache) {
+		// TODO: The order in which entities are saved may matter (e.g., a particular transient entity
+		//       may need to be saved before other transient entities can be saved;
+		//       Keep retrying the batch of transient entities until either:
+		//       1) there are no transient entities left in transientCopyCache
+		//       or 2) no transient entities were saved in the last batch
+		// For now, just run through the transient entities and retry the merge
+		for ( Iterator it=transientCopyCache.entrySet().iterator(); it.hasNext(); ) {
+			Map.Entry mapEntry = ( Map.Entry ) it.next();
+			Object entity = mapEntry.getKey();
+			Object copy = transientCopyCache.get( entity );
+			EntityEntry copyEntry = event.getSession().getPersistenceContext().getEntry( copy );
+			if ( entity == event.getEntity() ) {
+				mergeTransientEntity( entity, copyEntry.getEntityName(), event.getRequestedId(), event.getSession(), copyCache);
+			}
+			else {
+				mergeTransientEntity( entity, copyEntry.getEntityName(), copyEntry.getId(), event.getSession(), copyCache);
+			}
+		}
+	}
+
 	/** 
 	 * Handle the given merge event.
 	 *
 	 * @param event The merge event to be handled.
 	 * @throws HibernateException
 	 */
-	public void onMerge(MergeEvent event, Map copyCache) throws HibernateException {
+	public void onMerge(MergeEvent event, Map copiedAlready) throws HibernateException {
 
+		final CopyCache copyCache = ( CopyCache ) copiedAlready;
 		final EventSource source = event.getSession();
 		final Object original = event.getOriginal();
 
@@ -127,13 +190,17 @@
 			else {
 				entity = original;
 			}
-			
-			if ( copyCache.containsKey(entity) &&
-					source.getContextEntityIdentifier( copyCache.get( entity ) ) != null ) {
-				log.trace("already merged");
-				event.setResult(entity);
+
+			if ( copyCache.containsKey( entity ) &&
+					( copyCache.isIncludedInMerge( entity ) ) ) {
+				log.trace("already in merge process");
+				event.setResult( entity );				
 			}
 			else {
+				if ( copyCache.containsKey( entity ) ) {
+					log.trace("already in copyCache; setting in merge process");					
+					copyCache.setIncludedInMerge( entity, true );
+				}
 				event.setEntity( entity );
 				int entityState = -1;
 
@@ -175,7 +242,7 @@
 					default: //DELETED
 						throw new ObjectDeletedException(
 								"deleted instance passed to merge", 
-								null, 
+								null,
 								getLoggableName( event.getEntityName(), entity )
 							);			
 				}
@@ -193,15 +260,15 @@
 		final Object entity = event.getEntity();
 		final EventSource source = event.getSession();
 		final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
+
+		( ( CopyCache ) copyCache ).put( entity, entity, true  );  //before cascade!
 		
-		copyCache.put(entity, entity); //before cascade!
-		
 		cascadeOnMerge(source, persister, entity, copyCache);
 		copyValues(persister, entity, entity, source, copyCache);
 		
 		event.setResult(entity);
 	}
-	
+
 	protected void entityIsTransient(MergeEvent event, Map copyCache) {
 		
 		log.trace("merging transient instance");
@@ -211,7 +278,16 @@
 
 		final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
 		final String entityName = persister.getEntityName();
-		
+
+		event.setResult( mergeTransientEntity( entity, entityName, event.getRequestedId(), source, copyCache ) );
+	}
+
+	protected Object mergeTransientEntity(Object entity, String entityName, Serializable requestedId, EventSource source, Map copyCache) {
+
+		log.trace("merging transient instance");
+
+		final EntityPersister persister = source.getEntityPersister( entityName, entity );
+
 		final Serializable id = persister.hasIdentifierProperty() ?
 				persister.getIdentifier( entity, source.getEntityMode() ) :
 		        null;
@@ -219,7 +295,7 @@
 			persister.setIdentifier( copyCache.get( entity ), id, source.getEntityMode() );
 		}
 		else {
-			copyCache.put(entity, persister.instantiate( id, source.getEntityMode() ) ); //before cascade!
+			( ( CopyCache ) copyCache ).put( entity, persister.instantiate( id, source.getEntityMode() ), true ); //before cascade!
 			//TODO: should this be Session.instantiate(Persister, ...)?
 		}
 		final Object copy = copyCache.get( entity );
@@ -229,25 +305,54 @@
 		//cascadeOnMerge(event, persister, entity, copyCache, Cascades.CASCADE_BEFORE_MERGE);
 		super.cascadeBeforeSave(source, persister, entity, copyCache);
 		copyValues(persister, entity, copy, source, copyCache, ForeignKeyDirection.FOREIGN_KEY_FROM_PARENT);
-		
-		//this bit is only *really* absolutely necessary for handling 
-		//requestedId, but is also good if we merge multiple object 
-		//graphs, since it helps ensure uniqueness
-		final Serializable requestedId = event.getRequestedId();
-		if (requestedId==null) {
-			saveWithGeneratedId( copy, entityName, copyCache, source, false );
+
+		try {
+			//this bit is only *really* absolutely necessary for handling
+			//requestedId, but is also good if we merge multiple object
+			//graphs, since it helps ensure uniqueness
+			if (requestedId==null) {
+				saveWithGeneratedId( copy, entityName, copyCache, source, false );
+			}
+			else {
+				saveWithRequestedId( copy, requestedId, entityName, copyCache, source );
+			}
 		}
-		else {
-			saveWithRequestedId( copy, requestedId, entityName, copyCache, source );
+		catch (PropertyValueException ex) {
+			String propertyName = ex.getPropertyName();
+			Object propertyFromCopy = persister.getPropertyValue( copy, propertyName, source.getEntityMode() );
+			Object propertyFromEntity = persister.getPropertyValue( entity, propertyName, source.getEntityMode() );
+			Type propertyType = persister.getPropertyType( propertyName );
+			EntityEntry copyEntry = source.getPersistenceContext().getEntry( copy );
+			if ( propertyFromCopy == null || ! propertyType.isEntityType() ) {
+				log.trace( "property '" + copyEntry.getEntityName() + "." + propertyName +
+						"' is null or not an entity; " + propertyName + " =["+propertyFromCopy+"]");
+				throw ex;
+			}
+			if ( ! copyCache.containsKey( propertyFromEntity ) ) {
+				log.trace( "property '" + copyEntry.getEntityName() + "." + propertyName +
+						"' from original entity is not in copyCache; " + propertyName + " =["+propertyFromEntity+"]");
+				throw ex;
+			}
+			if ( ( ( CopyCache ) copyCache ).isIncludedInMerge( propertyFromEntity ) ) {
+				log.trace( "property '" + copyEntry.getEntityName() + "." + propertyName +
+						"' from original entity is in copyCache and is in the process of being merged; " +
+						propertyName + " =["+propertyFromEntity+"]");
+			}
+			else {
+				log.trace( "property '" + copyEntry.getEntityName() + "." + propertyName +
+						"' from original entity is in copyCache and is not in the process of being merged; " +
+						propertyName + " =["+propertyFromEntity+"]");
+			}
+			// continue...; we'll find out if it ends up not getting saved later
 		}
-		
-		// cascade first, so that all unsaved objects get their 
+
+		// cascade first, so that all unsaved objects get their
 		// copy created before we actually copy
 		super.cascadeAfterSave(source, persister, entity, copyCache);
 		copyValues(persister, entity, copy, source, copyCache, ForeignKeyDirection.FOREIGN_KEY_TO_PARENT);
-		
-		event.setResult(copy);
 
+		return copy;
+
 	}
 
 	protected void entityIsDetached(MergeEvent event, Map copyCache) {
@@ -259,7 +364,7 @@
 
 		final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity );
 		final String entityName = persister.getEntityName();
-			
+
 		Serializable id = event.getRequestedId();
 		if ( id == null ) {
 			id = persister.getIdentifier( entity, source.getEntityMode() );
@@ -292,7 +397,7 @@
 			entityIsTransient(event, copyCache);
 		}
 		else {
-			copyCache.put(entity, result); //before cascade!
+			( ( CopyCache ) copyCache ).put( entity, result, true ); //before cascade!
 	
 			final Object target = source.getPersistenceContext().unproxy(result);
 			if ( target == entity ) {
@@ -380,7 +485,7 @@
 			return entry.isExistsInDatabase();
 		}
 	}
-	
+
 	protected void copyValues(
 		final EntityPersister persister, 
 		final Object entity, 
@@ -388,7 +493,6 @@
 		final SessionImplementor source,
 		final Map copyCache
 	) {
-		
 		final Object[] copiedValues = TypeFactory.replace(
 				persister.getPropertyValues( entity, source.getEntityMode() ),
 				persister.getPropertyValues( target, source.getEntityMode() ),

Modified: core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/A.java
===================================================================
--- core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/A.java	2009-05-14 15:40:17 UTC (rev 16567)
+++ core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/A.java	2009-05-14 19:57:00 UTC (rev 16568)
@@ -1,4 +1,28 @@
-// $Id$
+//$Id: $
+/*
+ * 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.test.cascade;
 

Modified: core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/G.java
===================================================================
--- core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/G.java	2009-05-14 15:40:17 UTC (rev 16567)
+++ core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/G.java	2009-05-14 19:57:00 UTC (rev 16568)
@@ -1,3 +1,29 @@
+//$Id: $
+/*
+ * 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.test.cascade;
 
 import java.util.Set;

Modified: core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/H.java
===================================================================
--- core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/H.java	2009-05-14 15:40:17 UTC (rev 16567)
+++ core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/H.java	2009-05-14 19:57:00 UTC (rev 16568)
@@ -1,6 +1,29 @@
-// $Id$
+//$Id: $
+/*
+ * 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.test.cascade;
 
 import java.util.Set;

Modified: core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascadeTest.java
===================================================================
--- core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascadeTest.java	2009-05-14 15:40:17 UTC (rev 16567)
+++ core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascadeTest.java	2009-05-14 19:57:00 UTC (rev 16568)
@@ -1,4 +1,28 @@
 //$Id: $
+/*
+ * 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.test.cascade;
 

Added: core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParent.hbm.xml
===================================================================
--- core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParent.hbm.xml	                        (rev 0)
+++ core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParent.hbm.xml	2009-05-14 19:57:00 UTC (rev 16568)
@@ -0,0 +1,115 @@
+<?xml version="1.0"?>
+<!DOCTYPE hibernate-mapping SYSTEM "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd" >
+
+<hibernate-mapping package="org.hibernate.test.cascade.circle">
+
+    <class name="Route" table="HB_Route">
+
+        <id name="routeID" type="long"><generator class="native"/></id>
+        <version name="version" column="VERS" type="long" />
+
+        <property name="name" type="string" not-null="true"/>
+
+        <set name="nodes" inverse="true" cascade="persist,merge,refresh">
+            <key column="routeID"/>
+            <one-to-many class="Node"/>
+        </set>
+        <set name="vehicles" inverse="true" cascade="persist,merge,refresh">
+            <key column="routeID"/>
+            <one-to-many class="Vehicle"/>
+        </set>
+    </class>
+
+   <class name="Tour" table="HB_Tour">
+
+        <id name="tourID" type="long"><generator class="native"/></id>
+        <version name="version" column="VERS" type="long" />
+
+        <property name="name" type="string" not-null="true"/>
+
+        <set name="nodes" inverse="true" lazy="true" cascade="merge,refresh">
+            <key column="tourID"/>
+            <one-to-many class="Node"/>
+        </set>
+    </class>
+    
+    <class name="Transport" table="HB_Transport">
+
+        <id name="transportID" type="long"><generator class="native"/></id>
+        <version name="version" column="VERS" type="long" />
+
+        <property name="name" type="string" not-null="true"/>
+
+        <many-to-one name="pickupNode"
+            column="pickupNodeID"
+            unique="true"
+            not-null="true"
+            cascade="merge,refresh"
+            lazy="false"/> 
+
+        <many-to-one name="deliveryNode"
+            column="deliveryNodeID"
+            unique="true"
+            not-null="true"
+            cascade="merge,refresh"
+            lazy="false"/> 
+
+        <many-to-one name="vehicle"
+            column="vehicleID"
+            unique="false"
+            not-null="true"
+            cascade="none"
+            lazy="false"/>
+    </class>
+
+    <class name="Vehicle" table="HB_Vehicle">
+        <id name="vehicleID" type="long"><generator class="native"/></id>
+        <version name="version" column="VERS" type="long" />
+
+        <property name="name"/>
+        <set name="transports" inverse="false" lazy="true" cascade="merge,refresh">
+           <key column="vehicleID"/>
+           <one-to-many class="Transport" not-found="exception"/>
+       </set>
+        <many-to-one name="route"
+            column="routeID"
+            unique="false"
+            not-null="true"
+            cascade="none"
+            lazy="false"/>
+    </class>
+
+
+    <class name="Node" table="HB_Node">
+
+        <id name="nodeID" type="long"><generator class="native"/></id>
+        <version name="version" column="VERS" type="long" />
+
+        <property name="name" type="string" not-null="true"/>
+
+         <set name="deliveryTransports" inverse="true" lazy="true" cascade="merge,refresh">
+            <key column="deliveryNodeID"/>
+            <one-to-many class="Transport"/>
+        </set>
+
+        <set name="pickupTransports" inverse="true" lazy="true" cascade="merge,refresh">
+            <key column="pickupNodeID"/>
+            <one-to-many class="Transport"/>
+        </set>
+
+        <many-to-one name="route"
+            column="routeID"
+            unique="false"
+            not-null="true"
+            cascade="none"
+            lazy="false"/> 
+
+        <many-to-one name="tour"
+            column="tourID"
+            unique="false"
+            not-null="false"
+            cascade="merge,refresh"
+            lazy="false"/>                 
+    </class>
+
+</hibernate-mapping>

Added: core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParentTest.java
===================================================================
--- core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParentTest.java	                        (rev 0)
+++ core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParentTest.java	2009-05-14 19:57:00 UTC (rev 16568)
@@ -0,0 +1,288 @@
+//$Id: $
+/*
+ * 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.test.cascade.circle;
+
+import junit.framework.Test;
+
+import org.hibernate.Session;
+import org.hibernate.junit.functional.FunctionalTestCase;
+import org.hibernate.junit.functional.FunctionalTestClassTestSuite;
+
+/**
+ * The test case uses the following model:
+ *
+ *                         <-    ->
+ *                      -- (N : 0,1) -- Tour
+ *                      |    <-   ->
+ *                      | -- (1 : N) -- (pickup) ----
+ *               ->     | |                         |
+ * Route -- (1 : N) - Node                      Transport
+ *   |                    |  <-   ->                |  |
+ *   |                    -- (1 : N) -- (delivery) --  |
+ *   |                                                 |
+ *   |             ->                    ->            |
+ *   -------- (1 : N) ---- Vehicle--(1 : N)------------
+ *
+ * Arrows indicate the direction of cascade-merge.
+ * 
+ * I believe it reproduces the following issue:
+ *    http://opensource.atlassian.com/projects/hibernate/browse/HHH-3544
+ *
+ * @author Gail Badner (based on original model provided by Pavol Zibrita)
+ */
+public class CascadeMergeToChildBeforeParentTest extends FunctionalTestCase {
+
+	public CascadeMergeToChildBeforeParentTest(String string) {
+		super(string);
+	}
+
+	public String[] getMappings() {
+		return new String[] {
+				"cascade/circle/CascadeMergeToChildBeforeParent.hbm.xml"
+		};
+	}
+	
+	public static Test suite() {
+		return new FunctionalTestClassTestSuite( CascadeMergeToChildBeforeParentTest.class );
+	}
+	
+	protected void cleanupTest() {
+		Session s = openSession();
+		s.beginTransaction();
+		s.createQuery( "delete from Transport" );
+		s.createQuery( "delete from Tour" );
+		s.createQuery( "delete from Node" );
+		s.createQuery( "delete from Route" );
+		s.createQuery( "delete from Vehicle" );
+	}
+
+	public void testMerge()
+	{
+		Session s = openSession();
+		s.beginTransaction();
+
+		Route route = new Route();
+		route.setName("routeA");
+
+		s.save( route );
+		s.getTransaction().commit();
+		s.close();
+
+		s = openSession();
+		s.beginTransaction();
+
+		route = (Route) s.get(Route.class, new Long(1));
+
+		route.setTransientField(new String("sfnaouisrbn"));
+
+		Tour tour = new Tour();
+		tour.setName("tourB");
+
+		Node pickupNode = new Node();
+		pickupNode.setName("pickupNodeB");
+
+		Node deliveryNode = new Node();
+		deliveryNode.setName("deliveryNodeB");
+
+		pickupNode.setRoute(route);
+		pickupNode.setTour(tour);
+		pickupNode.setTransientField("pickup node aaaaaaaaaaa");
+
+		deliveryNode.setRoute(route);
+		deliveryNode.setTour(tour);
+		deliveryNode.setTransientField("delivery node aaaaaaaaa");
+
+		tour.getNodes().add(pickupNode);
+		tour.getNodes().add(deliveryNode);
+
+		route.getNodes().add(pickupNode);
+		route.getNodes().add(deliveryNode);
+
+		Route mergedRoute = (Route) s.merge(route);
+
+		s.getTransaction().commit();
+		s.close();
+	}
+
+	// This test fails because the merge algorithm tries to save a
+	// transient child (transport) before cascade-merge gets its
+	// transient parent (vehicle); merge does not cascade from the
+	// child to the parent.
+	public void testMergeTransientChildBeforeTransientParent()
+	{
+		Session s = openSession();
+		s.beginTransaction();
+
+		Route route = new Route();
+		route.setName("routeA");
+
+		s.save( route );
+		s.getTransaction().commit();
+		s.close();
+
+		s = openSession();
+		s.beginTransaction();
+
+		route = (Route) s.get(Route.class, new Long(1));
+
+		route.setTransientField(new String("sfnaouisrbn"));
+
+		Tour tour = new Tour();
+		tour.setName("tourB");
+
+		Transport transport = new Transport();
+		transport.setName("transportB");
+
+		Node pickupNode = new Node();
+		pickupNode.setName("pickupNodeB");
+
+		Node deliveryNode = new Node();
+		deliveryNode.setName("deliveryNodeB");
+
+		Vehicle vehicle = new Vehicle();
+		vehicle.setName("vehicleB");
+
+		pickupNode.setRoute(route);
+		pickupNode.setTour(tour);
+		pickupNode.getPickupTransports().add(transport);
+		pickupNode.setTransientField("pickup node aaaaaaaaaaa");
+
+		deliveryNode.setRoute(route);
+		deliveryNode.setTour(tour);
+		deliveryNode.getDeliveryTransports().add(transport);
+		deliveryNode.setTransientField("delivery node aaaaaaaaa");
+
+		tour.getNodes().add(pickupNode);
+		tour.getNodes().add(deliveryNode);
+
+		route.getNodes().add(pickupNode);
+		route.getNodes().add(deliveryNode);
+		route.getVehicles().add(vehicle);
+
+		transport.setPickupNode(pickupNode);
+		transport.setDeliveryNode(deliveryNode);
+		transport.setVehicle( vehicle );
+		transport.setTransientField("aaaaaaaaaaaaaa");
+
+		vehicle.getTransports().add(transport);
+		vehicle.setTransientField( "anewvalue" );
+		vehicle.setRoute( route );
+
+		Route mergedRoute = (Route) s.merge(route);
+
+		s.getTransaction().commit();
+		s.close();
+	}
+
+	public void testMergeData3Nodes()
+	{
+
+		Session s = openSession();
+		s.beginTransaction();
+
+		Route route = new Route();
+		route.setName("routeA");
+
+		s.save( route );
+		s.getTransaction().commit();
+		s.close();
+
+		s = openSession();
+		s.beginTransaction();
+
+		route = (Route) s.get(Route.class, new Long(1));
+
+		route.setTransientField(new String("sfnaouisrbn"));
+
+		Tour tour = new Tour();
+		tour.setName("tourB");
+
+		Transport transport1 = new Transport();
+		transport1.setName("TRANSPORT1");
+
+		Transport transport2 = new Transport();
+		transport2.setName("TRANSPORT2");
+
+		Node node1 = new Node();
+		node1.setName("NODE1");
+
+		Node node2 = new Node();
+		node2.setName("NODE2");
+
+		Node node3 = new Node();
+		node3.setName("NODE3");
+
+		Vehicle vehicle = new Vehicle();
+		vehicle.setName("vehicleB");
+
+		node1.setRoute(route);
+		node1.setTour(tour);
+		node1.getPickupTransports().add(transport1);
+		node1.setTransientField("node 1");
+
+		node2.setRoute(route);
+		node2.setTour(tour);
+		node2.getDeliveryTransports().add(transport1);
+		node2.getPickupTransports().add(transport2);
+		node2.setTransientField("node 2");
+
+		node3.setRoute(route);
+		node3.setTour(tour);
+		node3.getDeliveryTransports().add(transport2);
+		node3.setTransientField("node 3");
+
+		tour.getNodes().add(node1);
+		tour.getNodes().add(node2);
+		tour.getNodes().add(node3);
+
+		route.getNodes().add(node1);
+		route.getNodes().add(node2);
+		route.getNodes().add(node3);
+		route.getVehicles().add(vehicle);
+
+		transport1.setPickupNode(node1);
+		transport1.setDeliveryNode(node2);
+		transport1.setVehicle( vehicle );
+		transport1.setTransientField("aaaaaaaaaaaaaa");
+
+		transport2.setPickupNode(node2);
+		transport2.setDeliveryNode(node3);
+		transport2.setVehicle( vehicle );
+		transport2.setTransientField("bbbbbbbbbbbbb");
+
+		vehicle.getTransports().add(transport1);
+		vehicle.getTransports().add(transport2);
+		vehicle.setTransientField( "anewvalue" );
+		vehicle.setRoute( route );
+
+		Route mergedRoute = (Route) s.merge(route);
+
+		s.getTransaction().commit();
+		s.close();
+	}
+	
+}

Added: core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascade.hbm.xml
===================================================================
--- core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascade.hbm.xml	                        (rev 0)
+++ core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascade.hbm.xml	2009-05-14 19:57:00 UTC (rev 16568)
@@ -0,0 +1,82 @@
+<?xml version="1.0"?>
+<!DOCTYPE hibernate-mapping SYSTEM "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd" >
+
+<hibernate-mapping package="org.hibernate.test.cascade.circle">
+
+    <class name="Route" table="HB_Route">
+
+        <id name="routeID" type="long"><generator class="native"/></id>
+
+        <property name="name" type="string" not-null="true"/>
+
+        <set name="nodes" inverse="true" cascade="persist,merge,refresh">
+            <key column="routeID"/>
+            <one-to-many class="Node"/>
+        </set>
+    </class>
+
+   <class name="Tour" table="HB_Tour">
+
+        <id name="tourID" type="long"><generator class="native"/></id>
+
+        <property name="name" type="string" not-null="true"/>
+
+        <set name="nodes" inverse="true" lazy="true" cascade="merge,refresh">
+            <key column="tourID"/>
+            <one-to-many class="Node"/>
+        </set>
+    </class>
+    
+    <class name="Transport" table="HB_Transport">
+
+        <id name="transportID" type="long"><generator class="native"/></id>
+
+        <property name="name" type="string" not-null="true"/>
+
+        <many-to-one name="pickupNode"
+            column="pickupNodeID"
+            unique="true"
+            not-null="true"
+            cascade="merge,refresh"
+            lazy="false"/> 
+
+        <many-to-one name="deliveryNode"
+            column="deliveryNodeID"
+            unique="true"
+            not-null="true"
+            cascade="merge,refresh"
+            lazy="false"/> 
+    </class>
+
+    <class name="Node" table="HB_Node">
+
+        <id name="nodeID" type="long"><generator class="native"/></id>
+
+        <property name="name" type="string" not-null="true"/>
+
+         <set name="deliveryTransports" inverse="true" lazy="true" cascade="merge,refresh">
+            <key column="deliveryNodeID"/>
+            <one-to-many class="Transport"/>
+        </set>
+
+        <set name="pickupTransports" inverse="true" lazy="true" cascade="merge,refresh">
+            <key column="pickupNodeID"/>
+            <one-to-many class="Transport"/>
+        </set>
+
+        <many-to-one name="route"
+            column="routeID"
+            unique="false"
+            not-null="true"
+            cascade="none"
+            lazy="false"/> 
+
+        <many-to-one name="tour"
+            column="tourID"
+            unique="false"
+            not-null="false"
+            cascade="merge,refresh"
+            lazy="false"/>                 
+    </class>
+
+</hibernate-mapping>

Added: core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeTest.java
===================================================================
--- core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeTest.java	                        (rev 0)
+++ core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeTest.java	2009-05-14 19:57:00 UTC (rev 16568)
@@ -0,0 +1,459 @@
+//$Id: $
+/*
+ * 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.test.cascade.circle;
+
+import java.util.Iterator;
+
+import junit.framework.Test;
+
+import org.hibernate.Session;
+import org.hibernate.cfg.Configuration;
+import org.hibernate.cfg.Environment;
+import org.hibernate.junit.functional.FunctionalTestCase;
+import org.hibernate.junit.functional.FunctionalTestClassTestSuite;
+
+/**
+ * The test case uses the following model:
+ *
+ *                          <-    ->
+ *                      -- (N : 0,1) -- Tour
+ *                      |    <-   ->
+ *                      | -- (1 : N) -- (pickup) ----
+ *               ->     | |                          |
+ * Route -- (1 : N) -- Node                      Transport
+ *                      |  <-   ->                |
+ *                      -- (1 : N) -- (delivery) --
+ *
+ *  Arrows indicate the direction of cascade-merge.
+ *
+ * It reproduced the following issues:
+ *    http://opensource.atlassian.com/projects/hibernate/browse/HHH-3046
+ *    http://opensource.atlassian.com/projects/hibernate/browse/HHH-3810
+ *
+ * This tests that merge is cascaded properly from each entity.
+ * 
+ * @author Pavol Zibrita, Gail Badner
+ */
+public class MultiPathCircleCascadeTest extends FunctionalTestCase {
+
+	public MultiPathCircleCascadeTest(String string) {
+		super(string);
+	}
+
+	public void configure(Configuration cfg) {
+		cfg.setProperty( Environment.GENERATE_STATISTICS, "true");
+		cfg.setProperty( Environment.STATEMENT_BATCH_SIZE, "0" );
+	}
+
+	public String[] getMappings() {
+		return new String[] {
+				"cascade/circle/MultiPathCircleCascade.hbm.xml"
+		};
+	}
+
+	public static Test suite() {
+		return new FunctionalTestClassTestSuite( MultiPathCircleCascadeTest.class );
+	}
+	
+	protected void cleanupTest() {
+		Session s = openSession();
+		s.beginTransaction();
+		s.createQuery( "delete from Transport" );
+		s.createQuery( "delete from Tour" );
+		s.createQuery( "delete from Node" );
+		s.createQuery( "delete from Route" );
+	}
+	
+	public void testMergeRoute()
+	{
+
+		Route route = getUpdatedDetachedEntity();
+
+		clearCounts();
+
+		Session s = openSession();
+		s.beginTransaction();
+
+		s.merge(route);
+
+		s.getTransaction().commit();
+		s.close();
+
+		assertInsertCount( 4 );
+		assertUpdateCount( 1 );
+
+		s = openSession();
+		s.beginTransaction();
+		route = ( Route ) s.get( Route.class, route.getRouteID() );
+		checkResults( route, true );
+		s.getTransaction().commit();
+		s.close();
+	}
+
+	public void testMergePickupNode()
+	{
+
+		Route route = getUpdatedDetachedEntity();
+
+		clearCounts();
+
+		Session s = openSession();
+		s.beginTransaction();
+
+		Iterator it=route.getNodes().iterator();
+		Node node = ( Node ) it.next();
+		Node pickupNode;
+		if ( node.getName().equals( "pickupNodeB") ) {
+			pickupNode = node;
+		}
+		else {
+			node = ( Node ) it.next();
+			assertEquals( "pickupNodeB", node.getName() );
+			pickupNode = node;
+		}
+
+		pickupNode = ( Node ) s.merge( pickupNode );
+
+		s.getTransaction().commit();
+		s.close();
+
+		assertInsertCount( 4 );
+		assertUpdateCount( 0 );
+
+		s = openSession();
+		s.beginTransaction();
+		route = ( Route ) s.get( Route.class, route.getRouteID() );
+		checkResults( route, false );
+		s.getTransaction().commit();
+		s.close();
+	}
+
+	public void testMergeDeliveryNode()
+	{
+
+		Route route = getUpdatedDetachedEntity();
+
+		clearCounts();
+
+		Session s = openSession();
+		s.beginTransaction();
+
+		Iterator it=route.getNodes().iterator();
+		Node node = ( Node ) it.next();
+		Node deliveryNode;
+		if ( node.getName().equals( "deliveryNodeB") ) {
+			deliveryNode = node;
+		}
+		else {
+			node = ( Node ) it.next();
+			assertEquals( "deliveryNodeB", node.getName() );
+			deliveryNode = node;
+		}
+
+		deliveryNode = ( Node ) s.merge( deliveryNode );
+
+		s.getTransaction().commit();
+		s.close();
+
+		assertInsertCount( 4 );
+		assertUpdateCount( 0 );
+
+		s = openSession();
+		s.beginTransaction();
+		route = ( Route ) s.get( Route.class, route.getRouteID() );
+		checkResults( route, false );
+		s.getTransaction().commit();
+		s.close();
+	}
+
+	public void testMergeTour()
+	{
+
+		Route route = getUpdatedDetachedEntity();
+
+		clearCounts();
+
+		Session s = openSession();
+		s.beginTransaction();
+
+		Tour tour = ( Tour ) s.merge( ( ( Node ) route.getNodes().toArray()[0]).getTour() );
+
+		s.getTransaction().commit();
+		s.close();
+
+		assertInsertCount( 4 );
+		assertUpdateCount( 0 );
+
+		s = openSession();
+		s.beginTransaction();
+		route = ( Route ) s.get( Route.class, route.getRouteID() );
+		checkResults( route, false );
+		s.getTransaction().commit();
+		s.close();
+	}
+
+	public void testMergeTransport()
+	{
+
+		Route route = getUpdatedDetachedEntity();
+
+		clearCounts();
+
+		Session s = openSession();
+		s.beginTransaction();
+
+		Node node = ( ( Node ) route.getNodes().toArray()[0]);
+		Transport transport;
+		if ( node.getPickupTransports().size() == 1 ) {
+			transport = ( Transport ) node.getPickupTransports().toArray()[0];
+		}
+		else {
+			transport = ( Transport ) node.getDeliveryTransports().toArray()[0];
+		}
+
+		transport = ( Transport ) s.merge( transport  );
+
+		s.getTransaction().commit();
+		s.close();
+
+		assertInsertCount( 4 );
+		assertUpdateCount( 0 );
+
+		s = openSession();
+		s.beginTransaction();
+		route = ( Route ) s.get( Route.class, route.getRouteID() );
+		checkResults( route, false );
+		s.getTransaction().commit();
+		s.close();
+	}
+
+	private Route getUpdatedDetachedEntity() {
+
+		Session s = openSession();
+		s.beginTransaction();
+
+		Route route = new Route();
+		route.setName("routeA");
+
+		s.save( route );
+		s.getTransaction().commit();
+		s.close();
+
+		route.setName( "new routeA" );
+		route.setTransientField(new String("sfnaouisrbn"));
+
+		Tour tour = new Tour();
+		tour.setName("tourB");
+
+		Transport transport = new Transport();
+		transport.setName("transportB");
+
+		Node pickupNode = new Node();
+		pickupNode.setName("pickupNodeB");
+
+		Node deliveryNode = new Node();
+		deliveryNode.setName("deliveryNodeB");
+
+		pickupNode.setRoute(route);
+		pickupNode.setTour(tour);
+		pickupNode.getPickupTransports().add(transport);
+		pickupNode.setTransientField("pickup node aaaaaaaaaaa");
+
+		deliveryNode.setRoute(route);
+		deliveryNode.setTour(tour);
+		deliveryNode.getDeliveryTransports().add(transport);
+		deliveryNode.setTransientField("delivery node aaaaaaaaa");
+
+		tour.getNodes().add(pickupNode);
+		tour.getNodes().add(deliveryNode);
+
+		route.getNodes().add(pickupNode);
+		route.getNodes().add(deliveryNode);
+
+		transport.setPickupNode(pickupNode);
+		transport.setDeliveryNode(deliveryNode);
+		transport.setTransientField("aaaaaaaaaaaaaa");
+
+		return route;
+	}
+
+	private void checkResults(Route route, boolean isRouteUpdated) {
+		// since merge is not cascaded to route, this method needs to
+		// know whether route is expected to be updated
+		if ( isRouteUpdated ) {
+			assertEquals( "new routeA", route.getName() );
+		}
+		assertEquals( 2, route.getNodes().size() );
+		Node deliveryNode = null;
+		Node pickupNode = null;
+		for( Iterator it=route.getNodes().iterator(); it.hasNext(); ) {
+			Node node = ( Node ) it.next();
+			if( "deliveryNodeB".equals( node.getName(  )  ) ) {
+				deliveryNode = node;
+			}
+			else if( "pickupNodeB".equals( node.getName() ) ) {
+				pickupNode = node;
+			}
+			else {
+				fail( "unknown node");
+			}
+		}
+		assertNotNull( deliveryNode );
+		assertSame( route, deliveryNode.getRoute() );
+		assertEquals( 1, deliveryNode.getDeliveryTransports().size() );
+		assertEquals( 0, deliveryNode.getPickupTransports().size() );
+		assertNotNull( deliveryNode.getTour() );
+		assertEquals( "node original value", deliveryNode.getTransientField() );
+
+		assertNotNull( pickupNode );
+		assertSame( route, pickupNode.getRoute() );
+		assertEquals( 0, pickupNode.getDeliveryTransports().size() );
+		assertEquals( 1, pickupNode.getPickupTransports().size() );
+		assertNotNull( pickupNode.getTour() );
+		assertEquals( "node original value", pickupNode.getTransientField() );
+
+		assertTrue( ! deliveryNode.getNodeID().equals( pickupNode.getNodeID() ) );
+		assertSame( deliveryNode.getTour(), pickupNode.getTour() );
+		assertSame( deliveryNode.getDeliveryTransports().iterator().next(),
+				pickupNode.getPickupTransports().iterator().next() );
+
+		Tour tour = deliveryNode.getTour();
+		Transport transport = ( Transport ) deliveryNode.getDeliveryTransports().iterator().next();
+
+		assertEquals( "tourB", tour.getName() );
+		assertEquals( 2, tour.getNodes().size() );
+		assertTrue( tour.getNodes().contains( deliveryNode ) );
+		assertTrue( tour.getNodes().contains( pickupNode ) );
+
+		assertEquals( "transportB", transport.getName() );
+		assertSame( deliveryNode, transport.getDeliveryNode() );
+		assertSame( pickupNode, transport.getPickupNode() );
+		assertEquals( "transport original value", transport.getTransientField() );
+	}
+
+	public void testMergeData3Nodes()
+	{
+
+		Session s = openSession();
+		s.beginTransaction();
+
+		Route route = new Route();
+		route.setName("routeA");
+
+		s.save( route );
+		s.getTransaction().commit();
+		s.close();
+
+		clearCounts();
+
+		s = openSession();
+		s.beginTransaction();
+
+		route = (Route) s.get(Route.class, new Long(1));
+		//System.out.println(route);
+		route.setName( "new routA" );
+
+		route.setTransientField(new String("sfnaouisrbn"));
+
+		Tour tour = new Tour();
+		tour.setName("tourB");
+
+		Transport transport1 = new Transport();
+		transport1.setName("TRANSPORT1");
+
+		Transport transport2 = new Transport();
+		transport2.setName("TRANSPORT2");
+
+		Node node1 = new Node();
+		node1.setName("NODE1");
+
+		Node node2 = new Node();
+		node2.setName("NODE2");
+
+		Node node3 = new Node();
+		node3.setName("NODE3");
+
+		node1.setRoute(route);
+		node1.setTour(tour);
+		node1.getPickupTransports().add(transport1);
+		node1.setTransientField("node 1");
+
+		node2.setRoute(route);
+		node2.setTour(tour);
+		node2.getDeliveryTransports().add(transport1);
+		node2.getPickupTransports().add(transport2);
+		node2.setTransientField("node 2");
+
+		node3.setRoute(route);
+		node3.setTour(tour);
+		node3.getDeliveryTransports().add(transport2);
+		node3.setTransientField("node 3");
+
+		tour.getNodes().add(node1);
+		tour.getNodes().add(node2);
+		tour.getNodes().add(node3);
+
+		route.getNodes().add(node1);
+		route.getNodes().add(node2);
+		route.getNodes().add(node3);
+
+		transport1.setPickupNode(node1);
+		transport1.setDeliveryNode(node2);
+		transport1.setTransientField("aaaaaaaaaaaaaa");
+
+		transport2.setPickupNode(node2);
+		transport2.setDeliveryNode(node3);
+		transport2.setTransientField("bbbbbbbbbbbbb");
+
+		Route mergedRoute = (Route) s.merge(route);
+
+		s.getTransaction().commit();
+		s.close();
+
+		assertInsertCount( 6 );
+		assertUpdateCount( 1 );
+	}
+
+	protected void clearCounts() {
+		getSessions().getStatistics().clear();
+	}
+
+	protected void assertInsertCount(int expected) {
+		int inserts = ( int ) getSessions().getStatistics().getEntityInsertCount();
+		assertEquals( "unexpected insert count", expected, inserts );
+	}
+
+	protected void assertUpdateCount(int expected) {
+		int updates = ( int ) getSessions().getStatistics().getEntityUpdateCount();
+		assertEquals( "unexpected update counts", expected, updates );
+	}
+
+	protected void assertDeleteCount(int expected) {
+		int deletes = ( int ) getSessions().getStatistics().getEntityDeleteCount();
+		assertEquals( "unexpected delete counts", expected, deletes );
+	}	
+}

Added: core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Node.java
===================================================================
--- core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Node.java	                        (rev 0)
+++ core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Node.java	2009-05-14 19:57:00 UTC (rev 16568)
@@ -0,0 +1,154 @@
+//$Id: $
+/*
+ * 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.test.cascade.circle;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.HashSet;
+
+public class Node {
+
+//	@Id
+//	@SequenceGenerator(name="NODE_SEQ", sequenceName="NODE_SEQ", initialValue=1, allocationSize=1)
+//	@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="NODE_SEQ")
+	private Long nodeID;
+
+	private long version;
+	
+	private String name;
+	
+	/** the list of orders that are delivered at this node */
+//	@OneToMany(fetch=FetchType.LAZY, cascade={CascadeType.MERGE, CascadeType.REFRESH}, mappedBy="deliveryNode")
+	private Set deliveryTransports = new HashSet();
+	
+	/** the list of orders that are picked up at this node */
+//	@OneToMany(fetch=FetchType.LAZY, cascade=CascadeType.ALL, mappedBy="pickupNode")
+	private Set pickupTransports = new HashSet();
+	
+	/** the route to which this node belongs */
+//	@ManyToOne(targetEntity=Route.class, optional=false, fetch=FetchType.EAGER)
+//	@JoinColumn(name="ROUTEID", nullable=false, insertable=true, updatable=true)
+	private Route route = null;
+	
+	/** the tour this node belongs to, null if this node does not belong to a tour (e.g first node of a route) */
+//	@ManyToOne(targetEntity=Tour.class, cascade={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}, optional=true, fetch=FetchType.LAZY)
+//	@JoinColumn(name="TOURID", nullable=true, insertable=true, updatable=true)
+	private Tour tour;
+	
+//	@Transient
+	private String transientField = "node original value";
+
+	public Set getDeliveryTransports() {
+		return deliveryTransports;
+	}
+
+	public void setDeliveryTransports(Set deliveryTransports) {
+		this.deliveryTransports = deliveryTransports;
+	}
+
+	public Set getPickupTransports() {
+		return pickupTransports;
+	}
+
+	public void setPickupTransports(Set pickupTransports) {
+		this.pickupTransports = pickupTransports;
+	}
+
+	public Long getNodeID() {
+		return nodeID;
+	}
+
+	public long getVersion() {
+		return version;
+	}
+
+	protected void setVersion(long version) {
+		this.version = version;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public Route getRoute() {
+		return route;
+	}
+
+	public void setRoute(Route route) {
+		this.route = route;
+	}
+
+	public Tour getTour() {
+		return tour;
+	}
+	
+	public void setTour(Tour tour) {
+		this.tour = tour;
+	}
+
+	public String toString()
+	{
+		StringBuffer buffer = new StringBuffer();
+		
+		buffer.append( name + " id: " + nodeID );
+		if ( route != null ) {
+			buffer.append( " route name: " ).append( route.getName() ).append( " tour name: " ).append( tour.getName() );
+		}
+		if ( pickupTransports != null ) {
+			for (Iterator it = pickupTransports.iterator(); it.hasNext();) {
+				buffer.append("Pickup transports: " + it.next());
+			}
+		}
+		
+		if ( deliveryTransports != null ) {
+			for (Iterator it = deliveryTransports.iterator(); it.hasNext();) {
+				buffer.append("Delviery transports: " + it.next());
+			}
+		}
+		
+		return buffer.toString();
+	}
+
+	public String getTransientField() {
+		return transientField;
+	}
+
+	public void setTransientField(String transientField) {
+		this.transientField = transientField;
+	}
+
+	protected void setNodeID(Long nodeID) {
+		this.nodeID = nodeID;
+	}
+
+}

Added: core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Route.java
===================================================================
--- core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Route.java	                        (rev 0)
+++ core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Route.java	2009-05-14 19:57:00 UTC (rev 16568)
@@ -0,0 +1,119 @@
+//$Id: $
+/*
+ * 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.test.cascade.circle;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.HashSet;
+
+
+public class Route {
+	
+//	@Id
+//	@SequenceGenerator(name="ROUTE_SEQ", sequenceName="ROUTE_SEQ", initialValue=1, allocationSize=1)
+//	@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="ROUTE_SEQ")
+	private Long routeID;
+
+	private long version;
+
+	/** A List of nodes contained in this route. */
+//	@OneToMany(targetEntity=Node.class, fetch=FetchType.EAGER, cascade=CascadeType.ALL, mappedBy="route")
+	private Set nodes = new HashSet();
+
+	private Set vehicles = new HashSet();
+
+	private String name;
+	
+//	@Transient
+	private String transientField = null;
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	protected Set getNodes() {
+		return nodes;
+	}
+
+	protected void setNodes(Set nodes) {
+		this.nodes = nodes;
+	}
+
+	protected Set getVehicles() {
+		return vehicles;
+	}
+
+	protected void setVehicles(Set vehicles) {
+		this.vehicles = vehicles;
+	}
+
+	protected void setRouteID(Long routeID) {
+		this.routeID = routeID;
+	}
+
+	public Long getRouteID() {
+		return routeID;
+	}
+	
+	public long getVersion() {
+		return version;
+	}
+
+	protected void setVersion(long version) {
+		this.version = version;
+	}
+
+	public String toString()
+	{
+		StringBuffer buffer = new StringBuffer();
+		
+		buffer.append("Route name: " + name + " id: " + routeID + " transientField: " + transientField + "\n");
+		for (Iterator it = nodes.iterator(); it.hasNext();) {
+			buffer.append("Node: " + (Node)it.next());
+		}
+		
+		for (Iterator it = vehicles.iterator(); it.hasNext();) {
+			buffer.append("Vehicle: " + (Vehicle)it.next());
+		}
+
+		return buffer.toString();
+	}
+
+	public String getTransientField() {
+		return transientField;
+	}
+
+	public void setTransientField(String transientField) {
+		this.transientField = transientField;
+	}
+}

Added: core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Tour.java
===================================================================
--- core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Tour.java	                        (rev 0)
+++ core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Tour.java	2009-05-14 19:57:00 UTC (rev 16568)
@@ -0,0 +1,80 @@
+//$Id: $
+/*
+ * 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.test.cascade.circle;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.HashSet;
+
+
+public class Tour {
+	
+//	@Id
+//	@SequenceGenerator(name="TOUR_SEQ", sequenceName="TOUR_SEQ", initialValue=1, allocationSize=1)
+//	@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="TOUR_SEQ")
+	private Long tourID;
+
+	private long version;
+
+	private String name;
+
+	/** A List of nodes contained in this tour. */
+//	@OneToMany(targetEntity=Node.class, fetch=FetchType.LAZY, cascade={CascadeType.MERGE, CascadeType.REFRESH}, mappedBy="tour")
+	private Set nodes = new HashSet(0);
+
+	public String getName() {
+		return name;
+	}
+
+	protected void setTourID(Long tourID) {
+		this.tourID = tourID;
+	}
+
+	public long getVersion() {
+		return version;
+	}
+
+	protected void setVersion(long version) {
+		this.version = version;
+	}	
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public Set getNodes() {
+		return nodes;
+	}
+
+	public void setNodes(Set nodes) {
+		this.nodes = nodes;
+	}
+
+	public Long getTourID() {
+		return tourID;
+	}
+}

Added: core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Transport.java
===================================================================
--- core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Transport.java	                        (rev 0)
+++ core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Transport.java	2009-05-14 19:57:00 UTC (rev 16568)
@@ -0,0 +1,120 @@
+//$Id: $
+/*
+ * 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.test.cascade.circle;
+
+
+public class Transport {
+	
+//	@Id
+//	@SequenceGenerator(name="TRANSPORT_SEQ", sequenceName="TRANSPORT_SEQ", initialValue=1, allocationSize=1)
+//	@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="TRANSPORT_SEQ")
+	private Long transportID;
+
+	private long version;
+
+	private String name;
+	
+	/** node value object at which the order is picked up */
+//	@ManyToOne(optional=false, cascade={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}, fetch=FetchType.EAGER)
+//	@JoinColumn(name="PICKUPNODEID", /*nullable=false,*/insertable=true, updatable=true)
+	private Node pickupNode = null;
+
+	/** node value object at which the order is delivered */
+//	@ManyToOne(optional=false, cascade={CascadeType.MERGE, CascadeType.PERSIST, CascadeType.REFRESH}, fetch=FetchType.EAGER)
+//	@JoinColumn(name="DELIVERYNODEID", /*nullable=false,*/ insertable=true, updatable=true)
+	private Node deliveryNode = null;
+
+	private Vehicle vehicle;
+	
+//	@Transient
+	private String transientField = "transport original value";
+
+	public Node getDeliveryNode() {
+		return deliveryNode;
+	}
+
+	public void setDeliveryNode(Node deliveryNode) {
+		this.deliveryNode = deliveryNode;
+	}
+
+	public Node getPickupNode() {
+		return pickupNode;
+	}
+
+	protected void setTransportID(Long transportID) {
+		this.transportID = transportID;
+	}
+
+	public void setPickupNode(Node pickupNode) {
+		this.pickupNode = pickupNode;
+	}
+
+	public Vehicle getVehicle() {
+		return vehicle;
+	}
+
+	public void setVehicle(Vehicle vehicle) {
+		this.vehicle = vehicle;
+	}
+
+	public Long getTransportID() {
+		return transportID;
+	}
+
+	public long getVersion() {
+		return version;
+	}
+
+	protected void setVersion(long version) {
+		this.version = version;
+	}
+	
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+	
+	public String toString()
+	{
+		StringBuffer buffer = new StringBuffer();
+		
+		buffer.append(name + " id: " + transportID + "\n");
+		
+		return buffer.toString();
+	}
+
+	public String getTransientField() {
+		return transientField;
+	}
+
+	public void setTransientField(String transientField) {
+		this.transientField = transientField;
+	}
+}

Added: core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Vehicle.java
===================================================================
--- core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Vehicle.java	                        (rev 0)
+++ core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/cascade/circle/Vehicle.java	2009-05-14 19:57:00 UTC (rev 16568)
@@ -0,0 +1,106 @@
+//$Id: $
+/*
+ * 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.test.cascade.circle;
+
+import java.util.Set;
+import java.util.HashSet;
+
+
+public class Vehicle {
+
+//	@Id
+//	@SequenceGenerator(name="TRANSPORT_SEQ", sequenceName="TRANSPORT_SEQ", initialValue=1, allocationSize=1)
+//	@GeneratedValue(strategy=GenerationType.SEQUENCE, generator="TRANSPORT_SEQ")
+	private Long vehicleID;
+
+	private long version;
+
+	private String name;
+
+	private Set transports = new HashSet();
+
+	private Route route;
+
+	private String transientField = "vehicle original value";
+
+	protected void setVehicleID(Long vehicleID) {
+		this.vehicleID = vehicleID;
+	}
+
+	public Long getVehicleID() {
+		return vehicleID;
+	}
+
+	public long getVersion() {
+		return version;
+	}
+
+	protected void setVersion(long version) {
+		this.version = version;
+	}
+
+	public Set getTransports() {
+		return transports;
+	}
+
+	public void setTransports(Set transports) {
+		this.transports = transports;
+	}
+
+	public Route getRoute() {
+		return route;
+	}
+
+	public void setRoute(Route route) {
+		this.route = route;
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public String toString()
+	{
+		StringBuffer buffer = new StringBuffer();
+
+		buffer.append(name + " id: " + vehicleID + "\n");
+
+		return buffer.toString();
+	}
+
+	public String getTransientField() {
+		return transientField;
+	}
+
+	public void setTransientField(String transientField) {
+		this.transientField = transientField;
+	}
+}
\ No newline at end of file

Modified: core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/ops/MergeTest.java
===================================================================
--- core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/ops/MergeTest.java	2009-05-14 15:40:17 UTC (rev 16567)
+++ core/branches/Branch_3_3/testsuite/src/test/java/org/hibernate/test/ops/MergeTest.java	2009-05-14 19:57:00 UTC (rev 16568)
@@ -187,7 +187,7 @@
 		// as a control measure, now update the node while it is detached and
 		// make sure we get an update as a result...
 		( ( Node ) parent.getChildren().iterator().next() ).setDescription( "child's new description" );
-		parent.getChildren().add( new Node( "second child" ) );
+		parent.addChild( new Node( "second child" ) );
 		s = openSession();
 		s.beginTransaction();
 		parent = ( Node ) s.merge( parent );




More information about the hibernate-commits mailing list