diff --git a/doc/reference/en/modules/filters.xml b/doc/reference/en/modules/filters.xml
index b8270da003..a324cb9263 100755
--- a/doc/reference/en/modules/filters.xml
+++ b/doc/reference/en/modules/filters.xml
@@ -55,7 +55,7 @@
The methods on Session are: enableFilter(String filterName),
getEnabledFilter(String filterName), and disableFilter(String filterName).
By default, filters are not enabled for a given session; they must be explcitly
- enabled through use of the Session.enabledFilter() method, which returns an
+ enabled through use of the Session.enableFilter() method, which returns an
instance of the Filter interface. Using the simple filter defined above, this
would look like:
diff --git a/src/org/hibernate/cache/QueryKey.java b/src/org/hibernate/cache/QueryKey.java
index 5ccdcf48e2..3448a0fa6a 100644
--- a/src/org/hibernate/cache/QueryKey.java
+++ b/src/org/hibernate/cache/QueryKey.java
@@ -51,6 +51,7 @@ public class QueryKey implements Serializable {
}
public boolean equals(Object other) {
+ if ( !( other instanceof QueryKey ) ) return false;
QueryKey that = (QueryKey) other;
if ( !sqlQueryString.equals(that.sqlQueryString) ) return false;
if ( !EqualsHelper.equals(firstRow, that.firstRow) || !EqualsHelper.equals(maxRows, that.maxRows) ) return false;
diff --git a/src/org/hibernate/dialect/DB2Dialect.java b/src/org/hibernate/dialect/DB2Dialect.java
index 6b6b081181..198d6baddc 100644
--- a/src/org/hibernate/dialect/DB2Dialect.java
+++ b/src/org/hibernate/dialect/DB2Dialect.java
@@ -362,4 +362,8 @@ public class DB2Dialect extends Dialect {
public boolean supportsEmptyInList() {
return false;
}
+
+ public boolean supportsLobValueChangePropogation() {
+ return false;
+ }
}
diff --git a/src/org/hibernate/event/def/DefaultMergeEventListener.java b/src/org/hibernate/event/def/DefaultMergeEventListener.java
index 5817ab7831..ee79d6e332 100755
--- a/src/org/hibernate/event/def/DefaultMergeEventListener.java
+++ b/src/org/hibernate/event/def/DefaultMergeEventListener.java
@@ -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 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener
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 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener
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 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener
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 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener
}
}
-
+
protected void entityIsPersistent(MergeEvent event, Map copyCache) {
log.trace("ignoring persistent instance");
@@ -168,10 +192,15 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener
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);
diff --git a/src/org/hibernate/type/EntityType.java b/src/org/hibernate/type/EntityType.java
index 556ca283cf..99f188e93d 100644
--- a/src/org/hibernate/type/EntityType.java
+++ b/src/org/hibernate/type/EntityType.java
@@ -250,13 +250,23 @@ public abstract class EntityType extends AbstractType implements AssociationType
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;
+ }
+ 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 );
}
- id = getIdentifierOrUniqueKeyType( session.getFactory() )
- .replace(id, null, session, owner, copyCache);
- return resolve( id, session, owner );
}
}
diff --git a/test/org/hibernate/test/cascade/MultiPathCascadeTest.java b/test/org/hibernate/test/cascade/MultiPathCascadeTest.java
index 4ec865ab70..2d66dc06f8 100644
--- a/test/org/hibernate/test/cascade/MultiPathCascadeTest.java
+++ b/test/org/hibernate/test/cascade/MultiPathCascadeTest.java
@@ -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 Ovidiu Feodorov
@@ -33,15 +32,23 @@ public class MultiPathCascadeTest extends FunctionalTestCase {
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 @@ public class MultiPathCascadeTest extends FunctionalTestCase {
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,7 +81,33 @@ public class MultiPathCascadeTest extends FunctionalTestCase {
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();
@@ -88,8 +121,8 @@ public class MultiPathCascadeTest extends FunctionalTestCase {
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 @@ public class MultiPathCascadeTest extends FunctionalTestCase {
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 @@ public class MultiPathCascadeTest extends FunctionalTestCase {
s.close();
}
-}
\ No newline at end of file
+}
diff --git a/test/org/hibernate/test/ops/MergeTest.java b/test/org/hibernate/test/ops/MergeTest.java
index 1a86c05c40..b7718b5368 100755
--- a/test/org/hibernate/test/ops/MergeTest.java
+++ b/test/org/hibernate/test/ops/MergeTest.java
@@ -4,6 +4,7 @@ package org.hibernate.test.ops;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
+import java.util.Set;
import junit.framework.Test;
@@ -561,6 +562,94 @@ public class MergeTest extends AbstractOperationTestCase {
// cleanup();
}
+ public void testMergeManagedUninitializedCollection() {
+
+ Session s = openSession();
+ Transaction tx = s.beginTransaction();
+ NumberedNode root = new NumberedNode( "root" );
+ root.addChild( new NumberedNode( "child" ) );
+ s.persist(root);
+ tx.commit();
+ s.close();
+
+ clearCounts();
+
+ NumberedNode newRoot = new NumberedNode( "root" );
+ newRoot.setId( root.getId() );
+
+ s = openSession();
+ tx = s.beginTransaction();
+ root = ( NumberedNode ) s.get( NumberedNode.class, new Long( root.getId() ) );
+ Set managedChildren = root.getChildren();
+ assertFalse( Hibernate.isInitialized( managedChildren ) );
+ newRoot.setChildren( managedChildren );
+ assertSame( root, s.merge( newRoot ) );
+ assertSame( managedChildren, root.getChildren() );
+ assertFalse( Hibernate.isInitialized( managedChildren ) );
+ tx.commit();
+
+ assertInsertCount(0);
+ assertUpdateCount(0);
+ assertDeleteCount(0);
+
+ tx = s.beginTransaction();
+ assertEquals(
+ s.createCriteria(NumberedNode.class)
+ .setProjection( Projections.rowCount() )
+ .uniqueResult(),
+ new Integer(2)
+ );
+ tx.commit();
+
+ s.close();
+
+// cleanup();
+ }
+
+ public void testMergeManagedInitializedCollection() {
+
+ Session s = openSession();
+ Transaction tx = s.beginTransaction();
+ NumberedNode root = new NumberedNode( "root" );
+ root.addChild( new NumberedNode( "child" ) );
+ s.persist(root);
+ tx.commit();
+ s.close();
+
+ clearCounts();
+
+ NumberedNode newRoot = new NumberedNode( "root" );
+ newRoot.setId( root.getId() );
+
+ s = openSession();
+ tx = s.beginTransaction();
+ root = ( NumberedNode ) s.get( NumberedNode.class, new Long( root.getId() ) );
+ Set managedChildren = root.getChildren();
+ Hibernate.initialize( managedChildren );
+ assertTrue( Hibernate.isInitialized( managedChildren ) );
+ newRoot.setChildren( managedChildren );
+ assertSame( root, s.merge( newRoot ) );
+ assertSame( managedChildren, root.getChildren() );
+ assertTrue( Hibernate.isInitialized( managedChildren ) );
+ tx.commit();
+
+ assertInsertCount(0);
+ assertUpdateCount(0);
+ assertDeleteCount(0);
+
+ tx = s.beginTransaction();
+ assertEquals(
+ s.createCriteria(NumberedNode.class)
+ .setProjection( Projections.rowCount() )
+ .uniqueResult(),
+ new Integer(2)
+ );
+ tx.commit();
+
+ s.close();
+
+// cleanup();
+ }
public void testRecursiveMergeTransient() {
Session s = openSession();
Transaction tx = s.beginTransaction();