Author: gbadner
Date: 2008-05-09 19:23:29 -0400 (Fri, 09 May 2008)
New Revision: 14656
Added:
core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/A.java
core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/G.java
core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/H.java
core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascade.hbm.xml
core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascadeTest.java
Modified:
core/trunk/core/src/main/java/org/hibernate/event/def/DefaultMergeEventListener.java
core/trunk/core/src/main/java/org/hibernate/type/EntityType.java
core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/CascadeSuite.java
Log:
HHH-3229 : Cascade merge transient entities regardless of property traversal order
Modified:
core/trunk/core/src/main/java/org/hibernate/event/def/DefaultMergeEventListener.java
===================================================================
---
core/trunk/core/src/main/java/org/hibernate/event/def/DefaultMergeEventListener.java 2008-05-09
17:07:31 UTC (rev 14655)
+++
core/trunk/core/src/main/java/org/hibernate/event/def/DefaultMergeEventListener.java 2008-05-09
23:23:29 UTC (rev 14656)
@@ -1,24 +1,28 @@
-//$Id: DefaultMergeEventListener.java 10784 2006-11-11 05:13:01Z steve.ebersole(a)jboss.com
$
+//$Id: DefaultMergeEventListener.java 14513 2008-04-17 23:05:11Z gbadner $
package org.hibernate.event.def;
import java.io.Serializable;
+import java.util.Iterator;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+
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,14 +105,13 @@
entity = original;
}
- if ( copyCache.containsKey(entity) ) {
+ if ( copyCache.containsKey(entity) &&
+ source.getContextEntityIdentifier( copyCache.get( entity ) ) != null ) {
log.trace("already merged");
event.setResult(entity);
}
else {
-
event.setEntity( entity );
-
int entityState = -1;
// Check the persistence context for an entry relating to this
@@ -116,7 +138,7 @@
if ( entityState == -1 ) {
entityState = getEntityState( entity, event.getEntityName(), entry, source );
}
-
+
switch (entityState) {
case DETACHED:
entityIsDetached(event, copyCache);
@@ -128,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 )
@@ -170,11 +192,16 @@
final Serializable id = persister.hasIdentifierProperty() ?
persister.getIdentifier( entity, source.getEntityMode() ) :
null;
+ 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 );
- final Object copy = persister.instantiate( id, source.getEntityMode() ); //TODO:
should this be Session.instantiate(Persister, ...)?
- copyCache.put(entity, copy); //before cascade!
-
- // cascade first, so that all unsaved objects get their
+ // cascade first, so that all unsaved objects get their
// copy created before we actually copy
//cascadeOnMerge(event, persister, entity, copyCache, Cascades.CASCADE_BEFORE_MERGE);
super.cascadeBeforeSave(source, persister, entity, copyCache);
@@ -330,7 +357,7 @@
return entry.isExistsInDatabase();
}
}
-
+
protected void copyValues(
final EntityPersister persister,
final Object entity,
@@ -353,8 +380,8 @@
protected void copyValues(
final EntityPersister persister,
- final Object entity,
- final Object target,
+ final Object entity,
+ final Object target,
final SessionImplementor source,
final Map copyCache,
final ForeignKeyDirection foreignKeyDirection) {
Modified: core/trunk/core/src/main/java/org/hibernate/type/EntityType.java
===================================================================
--- core/trunk/core/src/main/java/org/hibernate/type/EntityType.java 2008-05-09 17:07:31
UTC (rev 14655)
+++ core/trunk/core/src/main/java/org/hibernate/type/EntityType.java 2008-05-09 23:23:29
UTC (rev 14656)
@@ -1,4 +1,4 @@
-//$Id: EntityType.java 10777 2006-11-08 22:02:28Z steve.ebersole(a)jboss.com $
+//$Id: EntityType.java 14513 2008-04-17 23:05:11Z gbadner $
package org.hibernate.type;
import java.io.Serializable;
@@ -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 );
+ }
}
}
@@ -451,7 +461,7 @@
if ( value == null ) {
return "null";
}
-
+
EntityPersister persister = factory.getEntityPersister( associatedEntityName );
StringBuffer result = new StringBuffer().append( associatedEntityName );
@@ -467,11 +477,11 @@
else {
id = getIdentifier( value, persister, entityMode );
}
-
+
result.append( '#' )
.append( persister.getIdentifierType().toLoggableString( id, factory ) );
}
-
+
return result.toString();
}
@@ -537,7 +547,7 @@
return uniqueKeyPropertyName;
}
}
-
+
protected abstract boolean isNullable();
/**
@@ -584,9 +594,9 @@
* @throws HibernateException generally indicates problems performing the load.
*/
public Object loadByUniqueKey(
- String entityName,
- String uniqueKeyPropertyName,
- Object key,
+ String entityName,
+ String uniqueKeyPropertyName,
+ Object key,
SessionImplementor session) throws HibernateException {
final SessionFactoryImplementor factory = session.getFactory();
UniqueKeyLoadable persister = ( UniqueKeyLoadable ) factory.getEntityPersister(
entityName );
@@ -594,11 +604,11 @@
//TODO: implement caching?! proxies?!
EntityUniqueKey euk = new EntityUniqueKey(
- entityName,
- uniqueKeyPropertyName,
- key,
+ entityName,
+ uniqueKeyPropertyName,
+ key,
getIdentifierOrUniqueKeyType( factory ),
- session.getEntityMode(),
+ session.getEntityMode(),
session.getFactory()
);
Added: core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/A.java
===================================================================
--- core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/A.java
(rev 0)
+++ core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/A.java 2008-05-09
23:23:29 UTC (rev 14656)
@@ -0,0 +1,106 @@
+// $Id$
+
+package org.hibernate.test.cascade;
+
+import java.util.Set;
+import java.util.HashSet;
+
+/**
+ * @author <a href="mailto:ovidiu@feodorov.com">Ovidiu
Feodorov</a>
+ *
+ * Copyright 2008 Ovidiu Feodorov
+ *
+ */
+public class A
+{
+ // Constants
-----------------------------------------------------------------------------------
+
+ // Static
--------------------------------------------------------------------------------------
+
+ // Attributes
----------------------------------------------------------------------------------
+
+ private long id;
+
+ private String data;
+
+ // A 1 - * H
+ private Set hs;
+
+ // A 1 - 1 G
+ private G g;
+
+
+ // Constructors
--------------------------------------------------------------------------------
+
+ public A()
+ {
+ hs = new HashSet();
+ }
+
+ public A(String data)
+ {
+ this();
+ this.data = data;
+ }
+
+ // Public
--------------------------------------------------------------------------------------
+
+ public long getId()
+ {
+ return id;
+ }
+
+ public void setId(long id)
+ {
+ this.id = id;
+ }
+
+ public void setData(String data)
+ {
+ this.data = data;
+ }
+
+ public String getData()
+ {
+ return data;
+ }
+
+ public void setHs(Set hs)
+ {
+ this.hs = hs;
+ }
+
+ public Set getHs()
+ {
+ return hs;
+ }
+
+ public void setG(G g)
+ {
+ this.g = g;
+ }
+
+ public G getG()
+ {
+ return g;
+ }
+
+ public void addH(H h)
+ {
+ hs.add(h);
+ h.setA(this);
+ }
+
+ public String toString()
+ {
+ return "A[" + id + ", " + data + "]";
+ }
+
+ // Package protected
---------------------------------------------------------------------------
+
+ // Protected
-----------------------------------------------------------------------------------
+
+ // Private
-------------------------------------------------------------------------------------
+
+ // Inner classes
-------------------------------------------------------------------------------
+}
Property changes on: core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/A.java
___________________________________________________________________
Name: svn:executable
+ *
Modified: core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/CascadeSuite.java
===================================================================
---
core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/CascadeSuite.java 2008-05-09
17:07:31 UTC (rev 14655)
+++
core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/CascadeSuite.java 2008-05-09
23:23:29 UTC (rev 14656)
@@ -12,6 +12,7 @@
TestSuite suite = new TestSuite( "Cascade tests" );
suite.addTest( BidirectionalOneToManyCascadeTest.suite() );
suite.addTest( RefreshTest.suite() );
+ suite.addTest( MultiPathCascadeTest.suite() );
return suite;
}
}
Added: core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/G.java
===================================================================
--- core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/G.java
(rev 0)
+++ core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/G.java 2008-05-09
23:23:29 UTC (rev 14656)
@@ -0,0 +1,95 @@
+package org.hibernate.test.cascade;
+
+import java.util.Set;
+import java.util.HashSet;
+
+/**
+ * @author <a href="mailto:ovidiu@feodorov.com">Ovidiu
Feodorov</a>
+ *
+ * Copyright 2008 Ovidiu Feodorov
+ *
+ * @version <tt>$Revision$</tt>
+ *
+ * $Id$
+ */
+public class G
+{
+ // Constants
-----------------------------------------------------------------------------------
+
+ // Static
--------------------------------------------------------------------------------------
+
+ // Attributes
----------------------------------------------------------------------------------
+
+ private long id;
+
+ private String data;
+
+ // A 1 <-> 1 G
+ private A a;
+
+ // G * <-> * H
+ private Set hs;
+
+ // Constructors
--------------------------------------------------------------------------------
+
+ public G()
+ {
+ this(null);
+ }
+
+ public G(String data)
+ {
+ this.data = data;
+ hs = new HashSet();
+ }
+
+ // Public
--------------------------------------------------------------------------------------
+
+ public String getData()
+ {
+ return data;
+ }
+
+ public void setData(String data)
+ {
+ this.data = data;
+ }
+
+ public A getA()
+ {
+ return a;
+ }
+
+ public void setA(A a)
+ {
+ this.a = a;
+ }
+
+ public Set getHs()
+ {
+ return hs;
+ }
+
+ public void setHs(Set s)
+ {
+ hs = s;
+ }
+
+ // Package protected
---------------------------------------------------------------------------
+
+ long getId()
+ {
+ return id;
+ }
+
+ // Protected
-----------------------------------------------------------------------------------
+
+ // Private
-------------------------------------------------------------------------------------
+
+ private void setId(long id)
+ {
+ this.id = id;
+ }
+
+ // Inner classes
-------------------------------------------------------------------------------
+}
Property changes on: core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/G.java
___________________________________________________________________
Name: svn:executable
+ *
Added: core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/H.java
===================================================================
--- core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/H.java
(rev 0)
+++ core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/H.java 2008-05-09
23:23:29 UTC (rev 14656)
@@ -0,0 +1,94 @@
+// $Id$
+
+
+package org.hibernate.test.cascade;
+
+import java.util.Set;
+import java.util.HashSet;
+
+/**
+ * @author <a href="mailto:ovidiu@feodorov.com">Ovidiu
Feodorov</a>
+ *
+ * Copyright 2008 Ovidiu Feodorov
+ *
+ */
+public class H
+{
+ // Constants
-----------------------------------------------------------------------------------
+
+ // Static
--------------------------------------------------------------------------------------
+
+ // Attributes
----------------------------------------------------------------------------------
+
+ private long id;
+
+ private String data;
+
+ private A a;
+
+ // G * <-> * H
+ private Set gs;
+
+ // Constructors
--------------------------------------------------------------------------------
+
+ public H()
+ {
+ this(null);
+ }
+
+ public H(String data)
+ {
+ this.data = data;
+ gs = new HashSet();
+ }
+
+ // Public
--------------------------------------------------------------------------------------
+
+ public long getId()
+ {
+ return id;
+ }
+
+ public String getData()
+ {
+ return data;
+ }
+
+ public void setData(String data)
+ {
+ this.data = data;
+ }
+
+ public A getA()
+ {
+ return a;
+ }
+
+ public void setA(A a)
+ {
+ this.a = a;
+ }
+
+ public Set getGs()
+ {
+ return gs;
+ }
+
+ public void setGs(Set gs)
+ {
+ this.gs = gs;
+ }
+
+ // Package protected
---------------------------------------------------------------------------
+
+ // Protected
-----------------------------------------------------------------------------------
+
+ // Private
-------------------------------------------------------------------------------------
+
+ private void setId(long id)
+ {
+ this.id = id;
+ }
+
+ // Inner classes
-------------------------------------------------------------------------------
+}
Property changes on: core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/H.java
___________________________________________________________________
Name: svn:executable
+ *
Added:
core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascade.hbm.xml
===================================================================
---
core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascade.hbm.xml
(rev 0)
+++
core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascade.hbm.xml 2008-05-09
23:23:29 UTC (rev 14656)
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+<!DOCTYPE hibernate-mapping SYSTEM
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd" >
+
+<hibernate-mapping package="org.hibernate.test.cascade">
+
+ <class name="A" table="HB_A">
+
+ <id name="id" type="long"><generator
class="native"/></id>
+
+ <property name="data" type="string"
not-null="true"/>
+
+ <!--
+ Associations
+ -->
+
+ <set name="hs" inverse="true" cascade="all">
+ <key column="a_fk"/>
+ <one-to-many class="H"/>
+ </set>
+ <one-to-one name="g" class="G" property-ref="a"
cascade="all"/>
+
+ </class>
+
+ <class name="G" table="HB_G">
+
+ <id name="id" type="long"><generator
class="native"/></id>
+
+ <property name="data" type="string"
not-null="true"/>
+
+ <!--
+ Associations
+ -->
+
+ <set name="hs" inverse="true" table="HB_G_H"
cascade="all">
+ <key column="g_fk"/>
+ <many-to-many class="H" column="h_fk"/>
+ </set>
+
+ <many-to-one name="a"
+ column="aId"
+ unique="true"
+ not-null="false"/>
+
+ </class>
+
+ <class name="H" table="HB_H">
+
+ <id name="id" type="long"><generator
class="native"/></id>
+
+ <property name="data" type="string"
not-null="true"/>
+
+ <!--
+ Associations
+ -->
+
+ <!-- *NOT* cascaded -->
+ <set name="gs" table="HB_G_H">
+ <key column="h_fk"/>
+ <many-to-many class="G" column="g_fk"/>
+ </set>
+
+ <many-to-one name="a" column="a_fk"
class="A"/>
+
+ </class>
+
+
+</hibernate-mapping>
\ No newline at end of file
Added:
core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascadeTest.java
===================================================================
---
core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascadeTest.java
(rev 0)
+++
core/trunk/testsuite/src/test/java/org/hibernate/test/cascade/MultiPathCascadeTest.java 2008-05-09
23:23:29 UTC (rev 14656)
@@ -0,0 +1,337 @@
+//$Id: $
+
+package org.hibernate.test.cascade;
+
+import junit.framework.Test;
+
+import org.hibernate.Session;
+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@feodorov.com">Ovidiu
Feodorov</a>
+ * @author Gail Badner
+ *
+ */
+
+public class MultiPathCascadeTest extends FunctionalTestCase {
+
+ public MultiPathCascadeTest(String name) {
+ super( name );
+ }
+
+ public String[] getMappings() {
+ return new String[] {
+ "cascade/MultiPathCascade.hbm.xml"
+ };
+ }
+
+ public static Test suite() {
+ return new FunctionalTestClassTestSuite( MultiPathCascadeTest.class );
+ }
+
+ 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 );
+ 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() );
+ }
+
+ 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 );
+ s.getTransaction().commit();
+ s.close();
+
+ // modify detached entity
+ modifyEntity( a );
+
+ s = openSession();
+ s.beginTransaction();
+ 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
+
+ Session s = openSession();
+ s.beginTransaction();
+ A a = new A();
+ a.setData( "Anna" );
+ s.save( a );
+ s.getTransaction().commit();
+ s.close();
+
+ s = openSession();
+ s.beginTransaction();
+ // retrieve the previously saved instance from the database, and update it
+ a = ( A ) s.get( A.class, new Long( a.getId() ) );
+ modifyEntity( a );
+ s.getTransaction().commit();
+ s.close();
+
+ 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" );
+
+ H h = new H();
+ h.setData( "Hellen" );
+
+ a.setG( g );
+ g.setA( a );
+
+ a.getHs().add( h );
+ h.setA( a );
+
+ g.getHs().add( h );
+ h.getGs().add( g );
+ }
+
+ private void verifyModifications(long aId) {
+ Session s = openSession();
+ s.beginTransaction();
+
+ // retrieve the A object and check it
+ A a = ( A ) s.get( A.class, new Long( aId ) );
+ assertEquals( aId, a.getId() );
+ assertEquals( "Anthony", a.getData() );
+ assertNotNull( a.getG() );
+ assertNotNull( a.getHs() );
+ assertEquals( 1, a.getHs().size() );
+
+ G gFromA = a.getG();
+ H hFromA = ( H ) a.getHs().iterator().next();
+
+ // check the G object
+ assertEquals( "Giovanni", gFromA.getData() );
+ assertSame( a, gFromA.getA() );
+ assertNotNull( gFromA.getHs() );
+ assertEquals( a.getHs(), gFromA.getHs() );
+ assertSame( hFromA, gFromA.getHs().iterator().next() );
+
+ // check the H object
+ assertEquals( "Hellen", hFromA.getData() );
+ assertSame( a, hFromA.getA() );
+ assertNotNull( hFromA.getGs() );
+ assertEquals( 1, hFromA.getGs().size() );
+ assertSame( gFromA, hFromA.getGs().iterator().next() );
+
+ s.getTransaction().commit();
+ s.close();
+ }
+
+}