[hibernate-commits] Hibernate SVN: r14513 - in core/branches/Branch_3_2: src/org/hibernate/type and 1 other directories.

hibernate-commits at lists.jboss.org hibernate-commits at lists.jboss.org
Thu Apr 17 19:05:11 EDT 2008


Author: gbadner
Date: 2008-04-17 19:05:11 -0400 (Thu, 17 Apr 2008)
New Revision: 14513

Modified:
   core/branches/Branch_3_2/src/org/hibernate/event/def/DefaultMergeEventListener.java
   core/branches/Branch_3_2/src/org/hibernate/type/EntityType.java
   core/branches/Branch_3_2/test/org/hibernate/test/cascade/MultiPathCascadeTest.java
Log:
HHH-3229 : Cascade merge transient entities regardless of property traversal order


Modified: core/branches/Branch_3_2/src/org/hibernate/event/def/DefaultMergeEventListener.java
===================================================================
--- core/branches/Branch_3_2/src/org/hibernate/event/def/DefaultMergeEventListener.java	2008-04-15 01:29:20 UTC (rev 14512)
+++ core/branches/Branch_3_2/src/org/hibernate/event/def/DefaultMergeEventListener.java	2008-04-17 23:05:11 UTC (rev 14513)
@@ -2,23 +2,27 @@
 package org.hibernate.event.def;
 
 import java.io.Serializable;
+import java.util.Iterator;
 import java.util.Map;
 
 import org.apache.commons.logging.Log;
 import org.apache.commons.logging.LogFactory;
+
 import org.hibernate.AssertionFailure;
 import org.hibernate.HibernateException;
 import org.hibernate.ObjectDeletedException;
 import org.hibernate.StaleObjectStateException;
+import org.hibernate.TransientObjectException;
 import org.hibernate.WrongClassException;
 import org.hibernate.engine.Cascade;
 import org.hibernate.engine.CascadingAction;
+import org.hibernate.engine.EntityEntry;
+import org.hibernate.engine.EntityKey;
+import org.hibernate.engine.SessionImplementor;
+import org.hibernate.engine.Status;
 import org.hibernate.event.EventSource;
 import org.hibernate.event.MergeEvent;
 import org.hibernate.event.MergeEventListener;
-import org.hibernate.engine.SessionImplementor;
-import org.hibernate.engine.EntityEntry;
-import org.hibernate.engine.EntityKey;
 import org.hibernate.intercept.FieldInterceptionHelper;
 import org.hibernate.intercept.FieldInterceptor;
 import org.hibernate.persister.entity.EntityPersister;
@@ -43,14 +47,33 @@
 		return IdentityMap.invert( (Map) anything );
 	}
 
-	/** 
+	/**
 	 * Handle the given merge event.
 	 *
 	 * @param event The merge event to be handled.
 	 * @throws HibernateException
 	 */
 	public void onMerge(MergeEvent event) throws HibernateException {
-		onMerge( event, IdentityMap.instantiate(10) );
+		Map copyCache = IdentityMap.instantiate(10);
+		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 ) {
+				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
+			}
+			if ( entry.getStatus() != Status.MANAGED ) {
+				throw new AssertionFailure( "Merged entity does not have status set to MANAGED; "+entry+" status="+entry.getStatus() );
+			}
+		}
 	}
 
 	/** 
@@ -82,7 +105,8 @@
 				entity = original;
 			}
 			
-			if ( copyCache.containsKey(entity) ) {
+			if ( copyCache.containsKey(entity) &&
+					source.getContextEntityIdentifier( copyCache.get( entity ) ) != null ) {
 				log.trace("already merged");
 				event.setResult(entity);
 			}
@@ -126,7 +150,7 @@
 						entityIsPersistent(event, copyCache);
 						break;
 					default: //DELETED
-						throw new ObjectDeletedException( 
+						throw new ObjectDeletedException(
 								"deleted instance passed to merge", 
 								null, 
 								getLoggableName( event.getEntityName(), entity )
@@ -137,7 +161,7 @@
 		}
 		
 	}
-	
+
 	protected void entityIsPersistent(MergeEvent event, Map copyCache) {
 		log.trace("ignoring persistent instance");
 		
@@ -168,10 +192,15 @@
 		final Serializable id = persister.hasIdentifierProperty() ?
 				persister.getIdentifier( entity, source.getEntityMode() ) :
 		        null;
-		
-		final Object copy = persister.instantiate( id, source.getEntityMode() );  //TODO: should this be Session.instantiate(Persister, ...)?
-		copyCache.put(entity, copy); //before cascade!
-		
+		if ( copyCache.containsKey( entity ) ) {
+			persister.setIdentifier( copyCache.get( entity ), id, source.getEntityMode() );
+		}
+		else {
+			copyCache.put(entity, persister.instantiate( id, source.getEntityMode() ) ); //before cascade!
+			//TODO: should this be Session.instantiate(Persister, ...)?
+		}
+		final Object copy = copyCache.get( entity );
+
 		// cascade first, so that all unsaved objects get their
 		// copy created before we actually copy
 		//cascadeOnMerge(event, persister, entity, copyCache, Cascades.CASCADE_BEFORE_MERGE);

Modified: core/branches/Branch_3_2/src/org/hibernate/type/EntityType.java
===================================================================
--- core/branches/Branch_3_2/src/org/hibernate/type/EntityType.java	2008-04-15 01:29:20 UTC (rev 14512)
+++ core/branches/Branch_3_2/src/org/hibernate/type/EntityType.java	2008-04-17 23:05:11 UTC (rev 14513)
@@ -250,13 +250,23 @@
 			if ( original == target ) {
 				return target;
 			}
-			Object id = getIdentifier( original, session );
-			if ( id == null ) {
-				throw new AssertionFailure("cannot copy a reference to an object with a null id");
+			if ( session.getContextEntityIdentifier( original ) == null  &&
+					ForeignKeys.isTransient( associatedEntityName, original, Boolean.FALSE, session ) ) {
+				final Object copy = session.getFactory().getEntityPersister( associatedEntityName )
+						.instantiate( null, session.getEntityMode() );
+				//TODO: should this be Session.instantiate(Persister, ...)?
+				copyCache.put( original, copy );
+				return copy;
 			}
-			id = getIdentifierOrUniqueKeyType( session.getFactory() )
-					.replace(id, null, session, owner, copyCache);
-			return resolve( id, session, owner );
+			else {
+				Object id = getIdentifier( original, session );
+				if ( id == null ) {
+					throw new AssertionFailure("non-transient entity has a null id");
+				}
+				id = getIdentifierOrUniqueKeyType( session.getFactory() )
+						.replace(id, null, session, owner, copyCache);
+				return resolve( id, session, owner );
+			}
 		}
 	}
 

Modified: core/branches/Branch_3_2/test/org/hibernate/test/cascade/MultiPathCascadeTest.java
===================================================================
--- core/branches/Branch_3_2/test/org/hibernate/test/cascade/MultiPathCascadeTest.java	2008-04-15 01:29:20 UTC (rev 14512)
+++ core/branches/Branch_3_2/test/org/hibernate/test/cascade/MultiPathCascadeTest.java	2008-04-17 23:05:11 UTC (rev 14513)
@@ -2,14 +2,13 @@
 
 package org.hibernate.test.cascade;
 
-import java.util.Collections;
-
 import junit.framework.Test;
 
 import org.hibernate.Session;
-import org.hibernate.Transaction;
+import org.hibernate.TransientObjectException;
 import org.hibernate.junit.functional.FunctionalTestCase;
 import org.hibernate.junit.functional.FunctionalTestClassTestSuite;
+import org.hibernate.proxy.HibernateProxy;
 
 /**
  * @author <a href="mailto:ovidiu at feodorov.com">Ovidiu Feodorov</a>
@@ -33,15 +32,23 @@
 		return new FunctionalTestClassTestSuite( MultiPathCascadeTest.class );
 	}
 
-	public void testMultiPathMergeDetachedFailureExpected() throws Exception
+	protected void cleanupTest() {
+		Session s = openSession();
+		s.beginTransaction();
+		s.createQuery( "delete from A" );
+		s.createQuery( "delete from G" );
+		s.createQuery( "delete from H" );
+	}
+
+	public void testMultiPathMergeModifiedDetached() throws Exception
 	{
 		// persist a simple A in the database
 
 		Session s = openSession();
 		s.beginTransaction();
 		A a = new A();
-		a.setData("Anna");
-		s.save(a);
+		a.setData( "Anna" );
+		s.save( a );
 		s.getTransaction().commit();
 		s.close();
 
@@ -50,22 +57,22 @@
 
 		s = openSession();
 		s.beginTransaction();
-		s.merge(a);
+		a = ( A ) s.merge( a );
 		s.getTransaction().commit();
 		s.close();
 
 		verifyModifications( a.getId() );
 	}
 
-	public void testMultiPathUpdateDetached() throws Exception
+	public void testMultiPathMergeModifiedDetachedIntoProxy() throws Exception
 	{
 		// persist a simple A in the database
 
 		Session s = openSession();
 		s.beginTransaction();
 		A a = new A();
-		a.setData("Anna");
-		s.save(a);
+		a.setData( "Anna" );
+		s.save( a );
 		s.getTransaction().commit();
 		s.close();
 
@@ -74,13 +81,39 @@
 
 		s = openSession();
 		s.beginTransaction();
-		s.update(a);
+		A aLoaded = ( A ) s.load( A.class, new Long( a.getId() ) );
+		assertTrue( aLoaded instanceof HibernateProxy );
+		assertSame( aLoaded, s.merge( a ) );
 		s.getTransaction().commit();
 		s.close();
 
 		verifyModifications( a.getId() );
 	}
 
+	public void testMultiPathUpdateModifiedDetached() throws Exception
+	{
+		// persist a simple A in the database
+
+		Session s = openSession();
+		s.beginTransaction();
+		A a = new A();
+		a.setData( "Anna" );
+		s.save( a );
+		s.getTransaction().commit();
+		s.close();
+
+		// modify detached entity
+		modifyEntity( a );
+
+		s = openSession();
+		s.beginTransaction();
+		s.update( a );
+		s.getTransaction().commit();
+		s.close();
+
+		verifyModifications( a.getId() );
+	}
+
 	public void testMultiPathGetAndModify() throws Exception
 	{
 		// persist a simple A in the database
@@ -88,8 +121,8 @@
 		Session s = openSession();
 		s.beginTransaction();
 		A a = new A();
-		a.setData("Anna");
-		s.save(a);
+		a.setData( "Anna" );
+		s.save( a );
 		s.getTransaction().commit();
 		s.close();
 
@@ -104,24 +137,168 @@
 		verifyModifications( a.getId() );
 	}
 
+	public void testMultiPathMergeNonCascadedTransientEntityInCollection() throws Exception
+	{
+		// persist a simple A in the database
+
+		Session s = openSession();
+		s.beginTransaction();
+		A a = new A();
+		a.setData( "Anna" );
+		s.save( a );
+		s.getTransaction().commit();
+		s.close();
+
+		// modify detached entity
+		modifyEntity( a );
+
+		s = openSession();
+		s.beginTransaction();
+		a = ( A ) s.merge( a );
+		s.getTransaction().commit();
+		s.close();
+
+		verifyModifications( a.getId() );
+
+		// add a new (transient) G to collection in h
+		// there is no cascade from H to the collection, so this should fail when merged
+		assertEquals( 1, a.getHs().size() );
+		H h = ( H ) a.getHs().iterator().next();
+		G gNew = new G();
+		gNew.setData( "Gail" );
+		gNew.getHs().add( h );
+		h.getGs().add( gNew );
+
+		s = openSession();
+		s.beginTransaction();
+		try {
+			s.merge( a );
+			s.merge( h );
+			fail( "should have thrown TransientObjectException" );
+		}
+		catch ( TransientObjectException ex ) {
+			// expected
+		}
+		finally {
+			s.getTransaction().rollback();
+		}
+		s.close();
+	}
+
+	public void testMultiPathMergeNonCascadedTransientEntityInOneToOne() throws Exception
+	{
+		// persist a simple A in the database
+
+		Session s = openSession();
+		s.beginTransaction();
+		A a = new A();
+		a.setData( "Anna" );
+		s.save( a );
+		s.getTransaction().commit();
+		s.close();
+
+		// modify detached entity
+		modifyEntity( a );
+
+		s = openSession();
+		s.beginTransaction();
+		a = ( A ) s.merge( a );
+		s.getTransaction().commit();
+		s.close();
+
+		verifyModifications( a.getId() );
+
+		// change the one-to-one association from g to be a new (transient) A
+		// there is no cascade from G to A, so this should fail when merged
+		G g = a.getG();
+		a.setG( null );
+		A aNew = new A();
+		aNew.setData( "Alice" );
+		g.setA( aNew );
+		aNew.setG( g );
+
+		s = openSession();
+		s.beginTransaction();
+		try {
+			s.merge( a );
+			s.merge( g );
+			fail( "should have thrown TransientObjectException" );
+		}
+		catch ( TransientObjectException ex ) {
+			// expected
+		}
+		finally {
+			s.getTransaction().rollback();
+		}
+		s.close();
+	}
+
+	public void testMultiPathMergeNonCascadedTransientEntityInManyToOne() throws Exception
+	{
+		// persist a simple A in the database
+
+		Session s = openSession();
+		s.beginTransaction();
+		A a = new A();
+		a.setData( "Anna" );
+		s.save( a );
+		s.getTransaction().commit();
+		s.close();
+
+		// modify detached entity
+		modifyEntity( a );
+
+		s = openSession();
+		s.beginTransaction();
+		a = ( A ) s.merge( a );
+		s.getTransaction().commit();
+		s.close();
+
+		verifyModifications( a.getId() );
+
+		// change the many-to-one association from h to be a new (transient) A
+		// there is no cascade from H to A, so this should fail when merged
+		assertEquals( 1, a.getHs().size() );
+		H h = ( H ) a.getHs().iterator().next();
+		a.getHs().remove( h );
+		A aNew = new A();
+		aNew.setData( "Alice" );
+		aNew.addH( h );
+
+		s = openSession();
+		s.beginTransaction();
+		try {
+			s.merge( a );
+			s.merge( h );
+			fail( "should have thrown TransientObjectException" );
+		}
+		catch ( TransientObjectException ex ) {
+			// expected
+		}
+		finally {
+			s.getTransaction().rollback();
+		}
+		s.close();
+	}
+
 	private void modifyEntity(A a) {
 		// create a *circular* graph in detached entity
 		a.setData("Anthony");
 
 		G g = new G();
-		g.setData("Giovanni");
+		g.setData( "Giovanni" );
 
 		H h = new H();
-		h.setData("Hellen");
+		h.setData( "Hellen" );
 
-		a.setG(g);
-		g.setA(a);
+		a.setG( g );
+		g.setA( a );
 
-		a.getHs().add(h);
-		h.setA(a);
+		a.getHs().add( h );
+		h.setA( a );
 
-		g.getHs().add(h);
-		h.getGs().add(g);
+		g.getHs().add( h );
+		h.getGs().add( g );
 	}
 
 	private void verifyModifications(long aId) {
@@ -157,4 +334,4 @@
 		s.close();
 	}
 
-}
\ No newline at end of file
+}




More information about the hibernate-commits mailing list