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