From 710d983cd6ece69868caaaa3e06b7058f6bbbea2 Mon Sep 17 00:00:00 2001 From: Gail Badner Date: Thu, 14 May 2009 20:27:19 +0000 Subject: [PATCH] HHH-3810 : Transient entities can be inserted twice on merge git-svn-id: https://svn.jboss.org/repos/hibernate/core/trunk@16569 1b8cb986-b30d-0410-93ca-fae66ebed9b2 --- .../org/hibernate/event/def/CopyCache.java | 244 ++++++++++ .../event/def/DefaultMergeEventListener.java | 196 ++++++-- .../java/org/hibernate/test/cascade/A.java | 26 +- .../java/org/hibernate/test/cascade/G.java | 26 + .../java/org/hibernate/test/cascade/H.java | 27 +- .../test/cascade/MultiPathCascadeTest.java | 24 + .../CascadeMergeToChildBeforeParent.hbm.xml | 115 +++++ .../CascadeMergeToChildBeforeParentTest.java | 288 +++++++++++ .../circle/MultiPathCircleCascade.hbm.xml | 82 ++++ .../circle/MultiPathCircleCascadeTest.java | 459 ++++++++++++++++++ .../hibernate/test/cascade/circle/Node.java | 154 ++++++ .../hibernate/test/cascade/circle/Route.java | 119 +++++ .../hibernate/test/cascade/circle/Tour.java | 80 +++ .../test/cascade/circle/Transport.java | 120 +++++ .../test/cascade/circle/Vehicle.java | 106 ++++ .../org/hibernate/test/ops/MergeTest.java | 2 +- 16 files changed, 2018 insertions(+), 50 deletions(-) create mode 100644 core/src/main/java/org/hibernate/event/def/CopyCache.java create mode 100644 testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParent.hbm.xml create mode 100644 testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParentTest.java create mode 100644 testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascade.hbm.xml create mode 100644 testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeTest.java create mode 100644 testsuite/src/test/java/org/hibernate/test/cascade/circle/Node.java create mode 100644 testsuite/src/test/java/org/hibernate/test/cascade/circle/Route.java create mode 100644 testsuite/src/test/java/org/hibernate/test/cascade/circle/Tour.java create mode 100644 testsuite/src/test/java/org/hibernate/test/cascade/circle/Transport.java create mode 100644 testsuite/src/test/java/org/hibernate/test/cascade/circle/Vehicle.java diff --git a/core/src/main/java/org/hibernate/event/def/CopyCache.java b/core/src/main/java/org/hibernate/event/def/CopyCache.java new file mode 100644 index 0000000000..9b19629abb --- /dev/null +++ b/core/src/main/java/org/hibernate/event/def/CopyCache.java @@ -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 diff --git a/core/src/main/java/org/hibernate/event/def/DefaultMergeEventListener.java b/core/src/main/java/org/hibernate/event/def/DefaultMergeEventListener.java index 4392437959..df9538ae54 100755 --- a/core/src/main/java/org/hibernate/event/def/DefaultMergeEventListener.java +++ b/core/src/main/java/org/hibernate/event/def/DefaultMergeEventListener.java @@ -27,6 +27,8 @@ package org.hibernate.event.def; 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.ObjectDeletedException; 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.HibernateProxy; 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 @@ -67,7 +70,7 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener private static final Logger log = LoggerFactory.getLogger(DefaultMergeEventListener.class); protected Map getMergeMap(Object anything) { - return IdentityMap.invert( (Map) anything ); + return ( ( CopyCache ) anything ).getMergeMap(); } /** @@ -77,24 +80,83 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener * @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(); - } - EntityEntry entry = event.getSession().getPersistenceContext().getEntry( entity ); - if ( entry == null ) { + // 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( - "object references an unsaved transient instance - save the transient instance before merging: " + - event.getSession().guessEntityName( entity ) - ); - // TODO: cache the entity name somewhere so that it is available to this exception - // entity name will not be available for non-POJO entities + "one or more objects is an unsaved transient instance - save transient instance(s) before merging: " + + transientEntityNames ); } - 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() ); + } + 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 is an unsaved transient instance - save the transient instance before merging: " + + event.getSession().guessEntityName( copy ) + ); + } + 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); } } } @@ -105,8 +167,9 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener * @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 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener 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 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener default: //DELETED throw new ObjectDeletedException( "deleted instance passed to merge", - null, + null, getLoggableName( event.getEntityName(), entity ) ); } @@ -193,15 +260,15 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener final Object entity = event.getEntity(); final EventSource source = event.getSession(); final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); - - copyCache.put(entity, entity); //before cascade! + + ( ( CopyCache ) copyCache ).put( entity, entity, true ); //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 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener 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 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener 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,24 +305,53 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener //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; } @@ -259,7 +364,7 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener 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 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener 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 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener return entry.isExistsInDatabase(); } } - + protected void copyValues( final EntityPersister persister, final Object entity, @@ -388,7 +493,6 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener final SessionImplementor source, final Map copyCache ) { - final Object[] copiedValues = TypeFactory.replace( persister.getPropertyValues( entity, source.getEntityMode() ), persister.getPropertyValues( target, source.getEntityMode() ), diff --git a/testsuite/src/test/java/org/hibernate/test/cascade/A.java b/testsuite/src/test/java/org/hibernate/test/cascade/A.java index 0602c43df8..4eba0d17a5 100755 --- a/testsuite/src/test/java/org/hibernate/test/cascade/A.java +++ b/testsuite/src/test/java/org/hibernate/test/cascade/A.java @@ -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; diff --git a/testsuite/src/test/java/org/hibernate/test/cascade/G.java b/testsuite/src/test/java/org/hibernate/test/cascade/G.java index c3d6d5aed4..18c0e60d26 100755 --- a/testsuite/src/test/java/org/hibernate/test/cascade/G.java +++ b/testsuite/src/test/java/org/hibernate/test/cascade/G.java @@ -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; diff --git a/testsuite/src/test/java/org/hibernate/test/cascade/H.java b/testsuite/src/test/java/org/hibernate/test/cascade/H.java index 472833541d..97924cf990 100755 --- a/testsuite/src/test/java/org/hibernate/test/cascade/H.java +++ b/testsuite/src/test/java/org/hibernate/test/cascade/H.java @@ -1,5 +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; diff --git a/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascadeTest.java b/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascadeTest.java index 2d66dc06f8..3accfedbea 100644 --- a/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascadeTest.java +++ b/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascadeTest.java @@ -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; diff --git a/testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParent.hbm.xml b/testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParent.hbm.xml new file mode 100644 index 0000000000..e73fb33a7c --- /dev/null +++ b/testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParent.hbm.xml @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParentTest.java b/testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParentTest.java new file mode 100644 index 0000000000..8459f9c18e --- /dev/null +++ b/testsuite/src/test/java/org/hibernate/test/cascade/circle/CascadeMergeToChildBeforeParentTest.java @@ -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(); + } + +} diff --git a/testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascade.hbm.xml b/testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascade.hbm.xml new file mode 100644 index 0000000000..c6ce75600e --- /dev/null +++ b/testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascade.hbm.xml @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeTest.java b/testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeTest.java new file mode 100644 index 0000000000..88a88fca5d --- /dev/null +++ b/testsuite/src/test/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeTest.java @@ -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 ); + } +} diff --git a/testsuite/src/test/java/org/hibernate/test/cascade/circle/Node.java b/testsuite/src/test/java/org/hibernate/test/cascade/circle/Node.java new file mode 100644 index 0000000000..cc4b797d99 --- /dev/null +++ b/testsuite/src/test/java/org/hibernate/test/cascade/circle/Node.java @@ -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; + } + +} diff --git a/testsuite/src/test/java/org/hibernate/test/cascade/circle/Route.java b/testsuite/src/test/java/org/hibernate/test/cascade/circle/Route.java new file mode 100644 index 0000000000..e2af2296be --- /dev/null +++ b/testsuite/src/test/java/org/hibernate/test/cascade/circle/Route.java @@ -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; + } +} diff --git a/testsuite/src/test/java/org/hibernate/test/cascade/circle/Tour.java b/testsuite/src/test/java/org/hibernate/test/cascade/circle/Tour.java new file mode 100644 index 0000000000..5f9a54a645 --- /dev/null +++ b/testsuite/src/test/java/org/hibernate/test/cascade/circle/Tour.java @@ -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; + } +} diff --git a/testsuite/src/test/java/org/hibernate/test/cascade/circle/Transport.java b/testsuite/src/test/java/org/hibernate/test/cascade/circle/Transport.java new file mode 100644 index 0000000000..1fd1ee321b --- /dev/null +++ b/testsuite/src/test/java/org/hibernate/test/cascade/circle/Transport.java @@ -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; + } +} diff --git a/testsuite/src/test/java/org/hibernate/test/cascade/circle/Vehicle.java b/testsuite/src/test/java/org/hibernate/test/cascade/circle/Vehicle.java new file mode 100644 index 0000000000..efe21fccfc --- /dev/null +++ b/testsuite/src/test/java/org/hibernate/test/cascade/circle/Vehicle.java @@ -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 diff --git a/testsuite/src/test/java/org/hibernate/test/ops/MergeTest.java b/testsuite/src/test/java/org/hibernate/test/ops/MergeTest.java index c3a4f54a78..c903d4fac8 100755 --- a/testsuite/src/test/java/org/hibernate/test/ops/MergeTest.java +++ b/testsuite/src/test/java/org/hibernate/test/ops/MergeTest.java @@ -187,7 +187,7 @@ public class MergeTest extends AbstractOperationTestCase { // 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 );