HHH-3810 : Transient entities can be inserted twice on merge

git-svn-id: https://svn.jboss.org/repos/hibernate/core/branches/Branch_3_2@16561 1b8cb986-b30d-0410-93ca-fae66ebed9b2
This commit is contained in:
Gail Badner 2009-05-13 22:17:08 +00:00
parent 956e4d1e2a
commit 4d57eb957d
17 changed files with 2023 additions and 51 deletions

View File

@ -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 );
}
}

View File

@ -28,6 +28,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.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@ -38,6 +40,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;
@ -54,7 +57,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
@ -66,9 +69,9 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener
implements MergeEventListener {
private static final Log log = LogFactory.getLog(DefaultMergeEventListener.class);
protected Map getMergeMap(Object anything) {
return IdentityMap.invert( (Map) anything );
return ( ( CopyCache ) anything ).getMergeMap();
}
/**
@ -78,24 +81,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);
}
}
}
@ -106,8 +168,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();
@ -128,13 +191,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;
@ -176,7 +243,7 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener
default: //DELETED
throw new ObjectDeletedException(
"deleted instance passed to merge",
null,
null,
getLoggableName( event.getEntityName(), entity )
);
}
@ -194,15 +261,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");
@ -212,7 +279,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;
@ -220,7 +296,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 );
@ -230,24 +306,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;
}
@ -260,7 +365,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() );
@ -293,7 +398,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 ) {
@ -381,7 +486,7 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener
return entry.isExistsInDatabase();
}
}
protected void copyValues(
final EntityPersister persister,
final Object entity,
@ -389,7 +494,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() ),

View File

@ -22,6 +22,8 @@ import org.hibernate.test.cache.CacheSuite;
import org.hibernate.test.cascade.BidirectionalOneToManyCascadeTest;
import org.hibernate.test.cascade.MultiPathCascadeTest;
import org.hibernate.test.cascade.RefreshTest;
import org.hibernate.test.cascade.circle.CascadeMergeToChildBeforeParentTest;
import org.hibernate.test.cascade.circle.MultiPathCircleCascadeTest;
import org.hibernate.test.cfg.ListenerTest;
import org.hibernate.test.cid.CompositeIdTest;
import org.hibernate.test.collection.bag.PersistentBagTest;
@ -404,6 +406,8 @@ public class AllTests {
suite.addTest( BidirectionalOneToManyCascadeTest.suite() );
suite.addTest( RefreshTest.suite() );
suite.addTest( MultiPathCascadeTest.suite() );
suite.addTest( CascadeMergeToChildBeforeParentTest.suite() );
suite.addTest( MultiPathCircleCascadeTest.suite() );
suite.addTest( ListenerTest.suite() );
suite.addTest( BrokenCollectionEventTest.suite() );
suite.addTest( BidirectionalManyToManyBagToSetCollectionEventTest.suite() );

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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();
}
}

View File

@ -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>

View File

@ -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 );
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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 );