diff --git a/hibernate-core/src/main/java/org/hibernate/action/internal/AbstractEntityInsertAction.java b/hibernate-core/src/main/java/org/hibernate/action/internal/AbstractEntityInsertAction.java new file mode 100644 index 0000000000..cb9792a38f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/action/internal/AbstractEntityInsertAction.java @@ -0,0 +1,182 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2011, Red Hat Inc. 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 Inc. + * + * 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.action.internal; + +import java.io.Serializable; + +import org.hibernate.LockMode; +import org.hibernate.engine.internal.ForeignKeys; +import org.hibernate.engine.internal.NonNullableTransientDependencies; +import org.hibernate.engine.internal.Nullability; +import org.hibernate.engine.internal.Versioning; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.Status; +import org.hibernate.persister.entity.EntityPersister; + +/** + * A base class for entity insert actions. + * + * @author Gail Badner + */ +public abstract class AbstractEntityInsertAction extends EntityAction { + private transient Object[] state; + private final boolean isVersionIncrementDisabled; + private boolean isExecuted; + private boolean areTransientReferencesNullified; + + /** + * Constructs an AbstractEntityInsertAction object. + * + * @param id - the entity ID + * @param state - the entity state + * @param instance - the entity + * @param isVersionIncrementDisabled - true, if version increment should + * be disabled; false, otherwise + * @param persister - the entity persister + * @param session - the session + */ + protected AbstractEntityInsertAction( + Serializable id, + Object[] state, + Object instance, + boolean isVersionIncrementDisabled, + EntityPersister persister, + SessionImplementor session) { + super( session, id, instance, persister ); + this.state = state; + this.isVersionIncrementDisabled = isVersionIncrementDisabled; + this.isExecuted = false; + this.areTransientReferencesNullified = false; + } + + /** + * Returns the entity state. + * + * NOTE: calling {@link #nullifyTransientReferences()} can modify the + * entity state. + * @return the entity state. + * + * @see {@link #nullifyTransientReferences()} + */ + public Object[] getState() { + return state; + } + + /** + * Does this insert action need to be executed as soon as possible + * (e.g., to generate an ID)? + * @return true, if it needs to be executed as soon as possible; + * false, otherwise. + */ + public abstract boolean isEarlyInsert(); + + /** + * Find the transient unsaved entity dependencies that are non-nullable. + * @return the transient unsaved entity dependencies that are non-nullable, + * or null if there are none. + */ + public NonNullableTransientDependencies findNonNullableTransientEntities() { + return ForeignKeys.findNonNullableTransientEntities( + getPersister().getEntityName(), + getInstance(), + getState(), + isEarlyInsert(), + getSession() + ); + } + + /** + * Have transient references been nullified? + * + * @return true, if transient references have been nullified; false, otherwise. + * + * @see {@link #nullifyTransientReferences()} + */ + protected final boolean areTransientReferencesNullified() { + return areTransientReferencesNullified; + } + + /** + * Nullifies any references to transient entities in the entity state + * maintained by this action. This method must be called when an entity + * is made "managed" or when this action is executed, whichever is first. + * + * @see {@link #areTransientReferencesNullified()} + * @see {@link #makeEntityManaged() } + */ + protected final void nullifyTransientReferences() { + new ForeignKeys.Nullifier( getInstance(), false, isEarlyInsert(), getSession() ) + .nullifyTransientReferences( getState(), getPersister().getPropertyTypes() ); + new Nullability( getSession() ).checkNullability( getState(), getPersister(), false ); + areTransientReferencesNullified = true; + } + + /** + * Make the entity "managed" by the persistence context. + */ + public final void makeEntityManaged() { + if ( !areTransientReferencesNullified ) { + nullifyTransientReferences(); + } + Object version = Versioning.getVersion( getState(), getPersister() ); + getSession().getPersistenceContext().addEntity( + getInstance(), + ( getPersister().isMutable() ? Status.MANAGED : Status.READ_ONLY ), + getState(), + getEntityKey(), + version, + LockMode.WRITE, + isExecuted, + getPersister(), + isVersionIncrementDisabled, + false + ); + } + + /** + * Indicate that the action has executed. + */ + protected void markExecuted() { + this.isExecuted = true; + } + + /** + * Returns the {@link EntityKey}. + * @return the {@link EntityKey}. + */ + protected abstract EntityKey getEntityKey(); + + @Override + public void afterDeserialize(SessionImplementor session) { + super.afterDeserialize( session ); + // IMPL NOTE: non-flushed changes code calls this method with session == null... + // guard against NullPointerException + if ( session != null ) { + EntityEntry entityEntry = session.getPersistenceContext().getEntry( getInstance() ); + this.state = entityEntry.getLoadedState(); + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/action/internal/EntityIdentityInsertAction.java b/hibernate-core/src/main/java/org/hibernate/action/internal/EntityIdentityInsertAction.java index b92a7f96cb..000e07c530 100644 --- a/hibernate-core/src/main/java/org/hibernate/action/internal/EntityIdentityInsertAction.java +++ b/hibernate-core/src/main/java/org/hibernate/action/internal/EntityIdentityInsertAction.java @@ -27,7 +27,6 @@ import java.io.Serializable; import org.hibernate.AssertionFailure; import org.hibernate.HibernateException; -import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.event.service.spi.EventListenerGroup; @@ -38,33 +37,39 @@ import org.hibernate.event.spi.PreInsertEvent; import org.hibernate.event.spi.PreInsertEventListener; import org.hibernate.persister.entity.EntityPersister; -public final class EntityIdentityInsertAction extends EntityAction { +public final class EntityIdentityInsertAction extends AbstractEntityInsertAction { - private transient Object[] state; private final boolean isDelayed; private final EntityKey delayedEntityKey; + private EntityKey entityKey; //private CacheEntry cacheEntry; private Serializable generatedId; public EntityIdentityInsertAction( Object[] state, - Object instance, - EntityPersister persister, - SessionImplementor session, - boolean isDelayed) throws HibernateException { + Object instance, + EntityPersister persister, + boolean isVersionIncrementDisabled, + SessionImplementor session, + boolean isDelayed) throws HibernateException { super( - session, ( isDelayed ? generateDelayedPostInsertIdentifier() : null ), + state, instance, - persister + isVersionIncrementDisabled, + persister, + session ); - this.state = state; this.isDelayed = isDelayed; this.delayedEntityKey = isDelayed ? generateDelayedEntityKey() : null; } @Override public void execute() throws HibernateException { + if ( ! areTransientReferencesNullified() ) { + nullifyTransientReferences(); + } + final EntityPersister persister = getPersister(); final SessionImplementor session = getSession(); final Object instance = getInstance(); @@ -75,14 +80,17 @@ public final class EntityIdentityInsertAction extends EntityAction { // else inserted the same pk first, the insert would fail if ( !veto ) { - generatedId = persister.insert( state, instance, session ); + generatedId = persister.insert( getState(), instance, session ); if ( persister.hasInsertGeneratedProperties() ) { - persister.processInsertGeneratedProperties( generatedId, instance, state, session ); + persister.processInsertGeneratedProperties( generatedId, instance, getState(), session ); } //need to do that here rather than in the save event listener to let //the post insert events to have a id-filled entity when IDENTITY is used (EJB3) persister.setIdentifier( instance, generatedId, session ); getSession().getPersistenceContext().registerInsertedKey( getPersister(), generatedId ); + // TODO: decide where to do this... + entityKey = getSession().generateEntityKey( generatedId, persister ); + getSession().getPersistenceContext().checkUniqueness( entityKey, getInstance() ); } @@ -100,6 +108,7 @@ public final class EntityIdentityInsertAction extends EntityAction { session.getFactory().getStatisticsImplementor().insertEntity( getPersister().getEntityName() ); } + markExecuted(); } @Override @@ -135,7 +144,7 @@ public final class EntityIdentityInsertAction extends EntityAction { final PostInsertEvent event = new PostInsertEvent( getInstance(), generatedId, - state, + getState(), getPersister(), eventSource() ); @@ -152,7 +161,7 @@ public final class EntityIdentityInsertAction extends EntityAction { final PostInsertEvent event = new PostInsertEvent( getInstance(), generatedId, - state, + getState(), getPersister(), eventSource() ); @@ -167,7 +176,7 @@ public final class EntityIdentityInsertAction extends EntityAction { return false; // NO_VETO } boolean veto = false; - final PreInsertEvent event = new PreInsertEvent( getInstance(), null, state, getPersister(), eventSource() ); + final PreInsertEvent event = new PreInsertEvent( getInstance(), null, getState(), getPersister(), eventSource() ); for ( PreInsertEventListener listener : listenerGroup.listeners() ) { veto |= listener.onPreInsert( event ); } @@ -178,29 +187,29 @@ public final class EntityIdentityInsertAction extends EntityAction { return generatedId; } + // TODO: nothing seems to use this method; can it be renmoved? public EntityKey getDelayedEntityKey() { return delayedEntityKey; } + @Override + public boolean isEarlyInsert() { + return !isDelayed; + } + + @Override + protected EntityKey getEntityKey() { + return entityKey != null ? entityKey : delayedEntityKey; + } + private synchronized static DelayedPostInsertIdentifier generateDelayedPostInsertIdentifier() { return new DelayedPostInsertIdentifier(); } private EntityKey generateDelayedEntityKey() { if ( !isDelayed ) { - throw new AssertionFailure( "cannot request delayed entity-key for non-delayed post-insert-id generation" ); + throw new AssertionFailure( "cannot request delayed entity-key for early-insert post-insert-id generation" ); } return getSession().generateEntityKey( getDelayedId(), getPersister() ); } - - @Override - public void afterDeserialize(SessionImplementor session) { - super.afterDeserialize( session ); - // IMPL NOTE: non-flushed changes code calls this method with session == null... - // guard against NullPointerException - if ( session != null ) { - EntityEntry entityEntry = session.getPersistenceContext().getEntry( getInstance() ); - this.state = entityEntry.getLoadedState(); - } - } } diff --git a/hibernate-core/src/main/java/org/hibernate/action/internal/EntityInsertAction.java b/hibernate-core/src/main/java/org/hibernate/action/internal/EntityInsertAction.java index d9c31223ea..8ad9c1b257 100644 --- a/hibernate-core/src/main/java/org/hibernate/action/internal/EntityInsertAction.java +++ b/hibernate-core/src/main/java/org/hibernate/action/internal/EntityInsertAction.java @@ -31,6 +31,7 @@ import org.hibernate.cache.spi.CacheKey; import org.hibernate.cache.spi.entry.CacheEntry; import org.hibernate.engine.internal.Versioning; import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.event.service.spi.EventListenerGroup; @@ -41,30 +42,39 @@ import org.hibernate.event.spi.PreInsertEvent; import org.hibernate.event.spi.PreInsertEventListener; import org.hibernate.persister.entity.EntityPersister; -public final class EntityInsertAction extends EntityAction { +public final class EntityInsertAction extends AbstractEntityInsertAction { - private Object[] state; private Object version; private Object cacheEntry; public EntityInsertAction( - Serializable id, - Object[] state, - Object instance, - Object version, - EntityPersister persister, - SessionImplementor session) throws HibernateException { - super( session, id, instance, persister ); - this.state = state; + Serializable id, + Object[] state, + Object instance, + Object version, + EntityPersister persister, + boolean isVersionIncrementDisabled, + SessionImplementor session) throws HibernateException { + super( id, state, instance, isVersionIncrementDisabled, persister, session ); this.version = version; } - public Object[] getState() { - return state; + @Override + public boolean isEarlyInsert() { + return false; + } + + @Override + protected EntityKey getEntityKey() { + return getSession().generateEntityKey( getId(), getPersister() ); } @Override public void execute() throws HibernateException { + if ( ! areTransientReferencesNullified() ) { + nullifyTransientReferences(); + } + EntityPersister persister = getPersister(); SessionImplementor session = getSession(); Object instance = getInstance(); @@ -77,7 +87,7 @@ public final class EntityInsertAction extends EntityAction { if ( !veto ) { - persister.insert( id, state, instance, session ); + persister.insert( id, getState(), instance, session ); EntityEntry entry = session.getPersistenceContext().getEntry( instance ); if ( entry == null ) { @@ -87,11 +97,11 @@ public final class EntityInsertAction extends EntityAction { entry.postInsert(); if ( persister.hasInsertGeneratedProperties() ) { - persister.processInsertGeneratedProperties( id, instance, state, session ); + persister.processInsertGeneratedProperties( id, instance, getState(), session ); if ( persister.isVersionPropertyGenerated() ) { - version = Versioning.getVersion( state, persister ); + version = Versioning.getVersion( getState(), persister ); } - entry.postUpdate(instance, state, version); + entry.postUpdate(instance, getState(), version); } getSession().getPersistenceContext().registerInsertedKey( getPersister(), getId() ); @@ -102,7 +112,7 @@ public final class EntityInsertAction extends EntityAction { if ( isCachePutEnabled( persister, session ) ) { CacheEntry ce = new CacheEntry( - state, + getState(), persister, persister.hasUninitializedLazyProperties( instance ), version, @@ -127,6 +137,7 @@ public final class EntityInsertAction extends EntityAction { .insertEntity( getPersister().getEntityName() ); } + markExecuted(); } private void postInsert() { @@ -137,7 +148,7 @@ public final class EntityInsertAction extends EntityAction { final PostInsertEvent event = new PostInsertEvent( getInstance(), getId(), - state, + getState(), getPersister(), eventSource() ); @@ -154,7 +165,7 @@ public final class EntityInsertAction extends EntityAction { final PostInsertEvent event = new PostInsertEvent( getInstance(), getId(), - state, + getState(), getPersister(), eventSource() ); @@ -170,7 +181,7 @@ public final class EntityInsertAction extends EntityAction { if ( listenerGroup.isEmpty() ) { return veto; } - final PreInsertEvent event = new PreInsertEvent( getInstance(), getId(), state, getPersister(), eventSource() ); + final PreInsertEvent event = new PreInsertEvent( getInstance(), getId(), getState(), getPersister(), eventSource() ); for ( PreInsertEventListener listener : listenerGroup.listeners() ) { veto |= listener.onPreInsert( event ); } diff --git a/hibernate-core/src/main/java/org/hibernate/action/internal/UnresolvedEntityInsertActions.java b/hibernate-core/src/main/java/org/hibernate/action/internal/UnresolvedEntityInsertActions.java new file mode 100644 index 0000000000..789a797c78 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/action/internal/UnresolvedEntityInsertActions.java @@ -0,0 +1,302 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2011, Red Hat Inc. 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 Inc. + * + * 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.action.internal; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import org.jboss.logging.Logger; + +import org.hibernate.TransientObjectException; +import org.hibernate.engine.internal.NonNullableTransientDependencies; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.engine.spi.Status; +import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.internal.util.collections.IdentitySet; +import org.hibernate.pretty.MessageHelper; + +/** + * Tracks unresolved entity insert actions. + * + * An entity insert action is unresolved if the entity + * to be inserted has at least one non-nullable association with + * an unsaved transient entity, and the foreign key points to that + * unsaved transient entity. + * + * These references must be resolved before an insert action can be + * executed. + * + * @author Gail Badner + */ +public class UnresolvedEntityInsertActions { + private static final CoreMessageLogger LOG = Logger.getMessageLogger( + CoreMessageLogger.class, + UnresolvedEntityInsertActions.class.getName() + ); + private static final int INIT_LIST_SIZE = 5; + + private final Map dependenciesByAction = + new IdentityHashMap( INIT_LIST_SIZE ); + private final Map> dependentActionsByTransientEntity = + new IdentityHashMap>( INIT_LIST_SIZE ); + + /** + * Add an unresolved insert action. + * + * @param insert - unresolved insert action. + * @param dependencies - non-nullable transient dependencies + * (must be non-null and non-empty). + * + * @throws IllegalArgumentException if {@code dependencies is null or empty}. + */ + public void addUnresolvedEntityInsertAction(AbstractEntityInsertAction insert, NonNullableTransientDependencies dependencies) { + if ( dependencies == null || dependencies.isEmpty() ) { + throw new IllegalArgumentException( + "Attempt to add an unresolved insert action that has no non-nullable transient entities." + ); + } + if ( LOG.isTraceEnabled() ) { + LOG.tracev( + "Adding insert with non-nullable, transient entities; insert=[{0}], dependencies=[{1}]", + insert, + dependencies.toLoggableString( insert.getSession() ) + ); + } + dependenciesByAction.put( insert, dependencies ); + addDependenciesByTransientEntity( insert, dependencies ); + } + + /** + * Returns the unresolved insert actions. + * @return the unresolved insert actions. + */ + public Iterable getDependentEntityInsertActions() { + return dependenciesByAction.keySet(); + } + + /** + * Returns true if there are no unresolved entity insert actions. + * @return true, if there are no unresolved entity insert actions; false, otherwise. + */ + public boolean isEmpty() { + return dependenciesByAction.isEmpty(); + } + + @SuppressWarnings({ "unchecked" }) + private void addDependenciesByTransientEntity(AbstractEntityInsertAction insert, NonNullableTransientDependencies dependencies) { + for ( Object transientEntity : dependencies.getNonNullableTransientEntities() ) { + Set dependentActions = dependentActionsByTransientEntity.get( transientEntity ); + if ( dependentActions == null ) { + dependentActions = new IdentitySet(); + dependentActionsByTransientEntity.put( transientEntity, dependentActions ); + } + dependentActions.add( insert ); + } + } + + /** + * Resolve any dependencies on {@code managedEntity}. + * + * @param managedEntity - the managed entity name + * @param session - the session + * + * @return the insert actions that depended only on the specified entity. + * + * @throws IllegalArgumentException if {@code managedEntity} did not have managed or read-only status. + */ + @SuppressWarnings({ "unchecked" }) + public Set resolveDependentActions(Object managedEntity, SessionImplementor session) { + EntityEntry entityEntry = session.getPersistenceContext().getEntry( managedEntity ); + if ( entityEntry.getStatus() != Status.MANAGED && entityEntry.getStatus() != Status.READ_ONLY ) { + throw new IllegalArgumentException( "EntityEntry did not have status MANAGED or READ_ONLY: " + entityEntry ); + } + // Find out if there are any unresolved insertions that are waiting for the + // specified entity to be resolved. + Set dependentActions = dependentActionsByTransientEntity.remove( managedEntity ); + if ( dependentActions == null ) { + if ( LOG.isTraceEnabled() ) { + LOG.tracev( + "No unresolved entity inserts that depended on [{0}]", + MessageHelper.infoString( entityEntry.getEntityName(), entityEntry.getId() ) + ); + } + return Collections.emptySet(); //NOTE EARLY EXIT! + } + Set resolvedActions = new IdentitySet( ); + for ( AbstractEntityInsertAction dependentAction : dependentActions ) { + NonNullableTransientDependencies dependencies = dependenciesByAction.get( dependentAction ); + dependencies.resolveNonNullableTransientEntity( managedEntity ); + if ( dependencies.isEmpty() ) { + if ( LOG.isTraceEnabled() ) { + LOG.tracev( + "Entity insert [{0}] only depended on [{1}]; removing from [{2}]", + dependentAction, + MessageHelper.infoString( entityEntry.getEntityName(), entityEntry.getId() ), + getClass().getSimpleName() + ); + } + // dependentAction only depended on managedEntity.. + dependenciesByAction.remove( dependentAction ); + resolvedActions.add( dependentAction ); + } + } + if ( LOG.isTraceEnabled() && ! resolvedActions.isEmpty() ) { + LOG.tracev( "Remaining unresolved dependencies: ", toString() ); + } + return resolvedActions; + } + + /** + * Clear this {@link UnresolvedEntityInsertActions}. + */ + public void clear() { + dependenciesByAction.clear(); + dependentActionsByTransientEntity.clear(); + } + + /** + * Throw TransientObjectException if there are any unresolved entity + * insert actions. + * + * @param session - the session + * + * @throws TransientObjectException if there are any unresolved + * entity insert actions. + */ + public void throwTransientObjectExceptionIfNotEmpty(SessionImplementor session) { + if ( isEmpty() ) { + return; // EARLY RETURN + } + StringBuilder sb = new StringBuilder( + "Could not save one or more entities because of non-nullable associations with unsaved transient instance(s); save these transient instance(s) before saving the dependent entities.\n" + ); + boolean firstTransientEntity = true; + for ( Map.Entry> entry : dependentActionsByTransientEntity.entrySet() ) { + if ( firstTransientEntity ) { + firstTransientEntity = false; + } + else { + sb.append( '\n' ); + } + Object transientEntity = entry.getKey(); + Set propertyPaths = new TreeSet(); + for ( AbstractEntityInsertAction dependentAction : entry.getValue() ) { + for ( String fullPropertyPaths : + dependenciesByAction.get( dependentAction ).getNonNullableTransientPropertyPaths( transientEntity ) ) { + propertyPaths.add( fullPropertyPaths ); + } + } + sb.append( "Non-nullable association" ); + if ( propertyPaths.size() > 1 ) { + sb.append( 's' ); + } + sb.append( " (" ); + boolean firstPropertyPath = true; + for ( String propertyPath : propertyPaths ) { + if ( firstPropertyPath ) { + firstPropertyPath = false; + } + else { + sb.append( ", " ); + } + sb.append( propertyPath ); + } + sb.append( ") depend" ); + if( propertyPaths.size() == 1 ) { + sb.append( 's' ); + } + sb.append( " on unsaved transient entity: " ) + .append( session.guessEntityName( transientEntity ) ) + .append( '.' ); + } + throw new TransientObjectException( sb.toString() ); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder( getClass().getSimpleName() ) + .append( '[' ); + for ( Map.Entry entry : dependenciesByAction.entrySet() ) { + AbstractEntityInsertAction insert = entry.getKey(); + NonNullableTransientDependencies dependencies = entry.getValue(); + sb.append( "[insert=" ) + .append( insert ) + .append( " dependencies=[" ) + .append( dependencies.toLoggableString( insert.getSession() ) ) + .append( "]" ); + } + sb.append( ']'); + return sb.toString(); + } + + /** + * Serialize this {@link UnresolvedEntityInsertActions} object. + * @param oos - the output stream + * @throws IOException if there is an error writing this object to the output stream. + */ + public void serialize(ObjectOutputStream oos) throws IOException { + int queueSize = dependenciesByAction.size(); + LOG.tracev( "Starting serialization of [{0}] unresolved insert entries", queueSize ); + oos.writeInt( queueSize ); + for ( AbstractEntityInsertAction unresolvedAction : dependenciesByAction.keySet() ) { + oos.writeObject( unresolvedAction ); + } + } + + /** + * Deerialize a {@link UnresolvedEntityInsertActions} object. + * + * @param ois - the input stream. + * @param session - the session. + * + * @return the deserialized {@link UnresolvedEntityInsertActions} object + * @throws IOException if there is an error writing this object to the output stream. + * @throws ClassNotFoundException if there is a class that cannot be loaded. + */ + public static UnresolvedEntityInsertActions deserialize( + ObjectInputStream ois, + SessionImplementor session) throws IOException, ClassNotFoundException { + + UnresolvedEntityInsertActions rtn = new UnresolvedEntityInsertActions(); + + int queueSize = ois.readInt(); + LOG.tracev( "Starting deserialization of [{0}] unresolved insert entries", queueSize ); + for ( int i = 0; i < queueSize; i++ ) { + AbstractEntityInsertAction unresolvedAction = ( AbstractEntityInsertAction ) ois.readObject(); + unresolvedAction.afterDeserialize( session ); + rtn.addUnresolvedEntityInsertAction( + unresolvedAction, + unresolvedAction.findNonNullableTransientEntities() + ); + } + return rtn; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/ForeignKeys.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/ForeignKeys.java index fa0c40467f..e6c42fef85 100755 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/ForeignKeys.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/ForeignKeys.java @@ -72,7 +72,7 @@ public final class ForeignKeys { values[i] = nullifyTransientReferences( values[i], types[i] ); } } - + /** * Return null if the argument is an "unsaved" entity (ie. * one with no existing database row), or the input argument @@ -169,9 +169,9 @@ public final class ForeignKeys { } } - + } - + /** * Is this instance persistent or detached? * If assumed is non-null, don't hit the database to make the @@ -257,4 +257,99 @@ public final class ForeignKeys { } } + /** + * Find all non-nullable references to entities that have not yet + * been inserted in the database, where the foreign key + * is a reference to an unsaved transient entity. . + * + * @param entityName - the entity name + * @param entity - the entity instance + * @param values - insertable properties of the object (including backrefs), + * possibly with substitutions + * @param isEarlyInsert - true if the entity needs to be executed as soon as possible + * (e.g., to generate an ID) + * @param session - the session + * + * @return the transient unsaved entity dependencies that are non-nullable, + * or null if there are none. + */ + public static NonNullableTransientDependencies findNonNullableTransientEntities( + String entityName, + Object entity, + Object[] values, + boolean isEarlyInsert, + SessionImplementor session + ) { + Nullifier nullifier = new Nullifier( entity, false, isEarlyInsert, session ); + final EntityPersister persister = session.getEntityPersister( entityName, entity ); + final String[] propertyNames = persister.getPropertyNames(); + final Type[] types = persister.getPropertyTypes(); + final boolean[] nullability = persister.getPropertyNullability(); + NonNullableTransientDependencies nonNullableTransientEntities = new NonNullableTransientDependencies(); + for ( int i = 0; i < types.length; i++ ) { + collectNonNullableTransientEntities( + entityName, + nullifier, + i, + values[i], + propertyNames[i], + types[i], + nullability[i], + session, + nonNullableTransientEntities + ); + } + return nonNullableTransientEntities.isEmpty() ? null : nonNullableTransientEntities; + } + + private static void collectNonNullableTransientEntities( + String entityName, + Nullifier nullifier, + int i, + Object value, + String propertyName, + Type type, + boolean isNullable, + SessionImplementor session, + NonNullableTransientDependencies nonNullableTransientEntities) { + if ( value == null ) { + return; // EARLY RETURN + } + if ( type.isEntityType() ) { + EntityType entityType = (EntityType) type; + if ( ! isNullable && + ! entityType.isOneToOne() && + nullifier.isNullifiable( entityType.getAssociatedEntityName(), value ) ) { + nonNullableTransientEntities.add( entityName, propertyName, value ); + } + } + else if ( type.isAnyType() ) { + if ( ! isNullable && + nullifier.isNullifiable( null, value ) ) { + nonNullableTransientEntities.add( entityName, propertyName, value ); + } + } + else if ( type.isComponentType() ) { + CompositeType actype = (CompositeType) type; + boolean[] subValueNullability = actype.getPropertyNullability(); + if ( subValueNullability != null ) { + String[] subPropertyNames = actype.getPropertyNames(); + Object[] subvalues = actype.getPropertyValues(value, session); + Type[] subtypes = actype.getSubtypes(); + for ( int j = 0; j < subvalues.length; j++ ) { + collectNonNullableTransientEntities( + entityName, + nullifier, + j, + subvalues[j], + subPropertyNames[j], + subtypes[j], + subValueNullability[j], + session, + nonNullableTransientEntities + ); + } + } + } + } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/NonNullableTransientDependencies.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/NonNullableTransientDependencies.java new file mode 100644 index 0000000000..36f5fc3da7 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/NonNullableTransientDependencies.java @@ -0,0 +1,81 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * Copyright (c) 2011, Red Hat Inc. 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 Inc. + * + * 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.engine.internal; + +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.Set; + +import org.hibernate.engine.spi.SessionImplementor; + +/** + * @author Gail Badner + */ +public class NonNullableTransientDependencies { + private final Map> propertyPathsByTransientEntity = + new IdentityHashMap>(); + + /* package-protected */ + void add(String entityName, String propertyName, Object transientEntity) { + Set propertyPaths = propertyPathsByTransientEntity.get( transientEntity ); + if ( propertyPaths == null ) { + propertyPaths = new HashSet(); + propertyPathsByTransientEntity.put( transientEntity, propertyPaths ); + } + StringBuilder sb = new StringBuilder( entityName.length() + propertyName.length() + 1 ) + .append( entityName ) + .append( '.' ) + .append( propertyName ); + propertyPaths.add( sb.toString() ); + } + + public Iterable getNonNullableTransientEntities() { + return propertyPathsByTransientEntity.keySet(); + } + + public Iterable getNonNullableTransientPropertyPaths(Object entity) { + return propertyPathsByTransientEntity.get( entity ); + } + + public boolean isEmpty() { + return propertyPathsByTransientEntity.isEmpty(); + } + + public void resolveNonNullableTransientEntity(Object entity) { + if ( propertyPathsByTransientEntity.remove( entity ) == null ) { + throw new IllegalStateException( "Attempt to resolve a non-nullable, transient entity that is not a dependency." ); + } + } + + public String toLoggableString(SessionImplementor session) { + StringBuilder sb = new StringBuilder( getClass().getSimpleName() ).append( '[' ); + for ( Map.Entry> entry : propertyPathsByTransientEntity.entrySet() ) { + sb.append( "transientEntityName=" ).append( session.bestGuessEntityName( entry.getKey() ) ); + sb.append( " requiredBy=" ).append( entry.getValue() ); + } + sb.append( ']' ); + return sb.toString(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java index a2ae1fc77c..af8232599b 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java @@ -38,11 +38,13 @@ import org.jboss.logging.Logger; import org.hibernate.AssertionFailure; import org.hibernate.HibernateException; +import org.hibernate.action.internal.AbstractEntityInsertAction; import org.hibernate.action.internal.BulkOperationCleanupAction; import org.hibernate.action.internal.CollectionAction; import org.hibernate.action.internal.CollectionRecreateAction; import org.hibernate.action.internal.CollectionRemoveAction; import org.hibernate.action.internal.CollectionUpdateAction; +import org.hibernate.action.internal.UnresolvedEntityInsertActions; import org.hibernate.action.internal.EntityAction; import org.hibernate.action.internal.EntityDeleteAction; import org.hibernate.action.internal.EntityIdentityInsertAction; @@ -52,6 +54,7 @@ import org.hibernate.action.spi.AfterTransactionCompletionProcess; import org.hibernate.action.spi.BeforeTransactionCompletionProcess; import org.hibernate.action.spi.Executable; import org.hibernate.cache.CacheException; +import org.hibernate.engine.internal.NonNullableTransientDependencies; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.type.Type; @@ -63,6 +66,7 @@ import org.hibernate.type.Type; * until a flush forces them to be executed against the database. * * @author Steve Ebersole + * @author Gail Badner */ public class ActionQueue { @@ -74,6 +78,7 @@ public class ActionQueue { // Object insertions, updates, and deletions have list semantics because // they must happen in the right order so as to respect referential // integrity + private UnresolvedEntityInsertActions unresolvedInsertions; private ArrayList insertions; private ArrayList deletions; private ArrayList updates; @@ -99,7 +104,8 @@ public class ActionQueue { } private void init() { - insertions = new ArrayList( INIT_QUEUE_LIST_SIZE ); + unresolvedInsertions = new UnresolvedEntityInsertActions(); + insertions = new ArrayList( INIT_QUEUE_LIST_SIZE ); deletions = new ArrayList( INIT_QUEUE_LIST_SIZE ); updates = new ArrayList( INIT_QUEUE_LIST_SIZE ); @@ -119,11 +125,14 @@ public class ActionQueue { collectionCreations.clear(); collectionRemovals.clear(); collectionUpdates.clear(); + + unresolvedInsertions.clear(); } @SuppressWarnings({ "unchecked" }) public void addAction(EntityInsertAction action) { - insertions.add( action ); + LOG.tracev( "Adding an EntityInsertAction for [{0}] object", action.getEntityName() ); + addInsertAction( action ); } @SuppressWarnings({ "unchecked" }) @@ -153,7 +162,62 @@ public class ActionQueue { @SuppressWarnings({ "unchecked" }) public void addAction(EntityIdentityInsertAction insert) { - insertions.add( insert ); + LOG.tracev( "Adding an EntityIdentityInsertAction for [{0}] object", insert.getEntityName() ); + addInsertAction( insert ); + } + + private void addInsertAction(AbstractEntityInsertAction insert) { + if ( insert.isEarlyInsert() ) { + // For early inserts, must execute inserts before finding non-nullable transient entities. + // TODO: find out why this is necessary + LOG.tracev( + "Executing inserts before finding non-nullable transient entities for early insert: [{0}]", + insert + ); + executeInserts(); + } + NonNullableTransientDependencies nonNullableTransientDependencies = insert.findNonNullableTransientEntities(); + if ( nonNullableTransientDependencies == null ) { + LOG.tracev( "Adding insert with no non-nullable, transient entities: [{0}]", insert); + addResolvedEntityInsertAction( insert ); + } + else { + if ( LOG.isTraceEnabled() ) { + LOG.tracev( + "Adding insert with non-nullable, transient entities; insert=[{0}], dependencies=[{1}]", + insert, + nonNullableTransientDependencies.toLoggableString( insert.getSession() ) + ); + } + unresolvedInsertions.addUnresolvedEntityInsertAction( insert, nonNullableTransientDependencies ); + } + } + + @SuppressWarnings({ "unchecked" }) + private void addResolvedEntityInsertAction(AbstractEntityInsertAction insert) { + if ( insert.isEarlyInsert() ) { + LOG.trace( "Executing insertions before resolved early-insert" ); + executeInserts(); + LOG.debug( "Executing identity-insert immediately" ); + execute( insert ); + } + else { + LOG.trace( "Adding resolved non-early insert action." ); + insertions.add( insert ); + } + insert.makeEntityManaged(); + for ( AbstractEntityInsertAction resolvedAction : + unresolvedInsertions.resolveDependentActions( insert.getInstance(), session ) ) { + addResolvedEntityInsertAction( resolvedAction ); + } + } + + public boolean hasUnresolvedEntityInsertActions() { + return ! unresolvedInsertions.isEmpty(); + } + + public void checkNoUnresolvedEntityInsertActions() { + unresolvedInsertions.throwTransientObjectExceptionIfNotEmpty( session ); } public void addAction(BulkOperationCleanupAction cleanupAction) { @@ -183,6 +247,7 @@ public class ActionQueue { * @throws HibernateException error executing queued actions. */ public void executeActions() throws HibernateException { + checkNoUnresolvedEntityInsertActions(); executeActions( insertions ); executeActions( updates ); executeActions( collectionRemovals ); @@ -230,6 +295,7 @@ public class ActionQueue { public boolean areTablesToBeUpdated(Set tables) { return areTablesToUpdated( updates, tables ) || areTablesToUpdated( insertions, tables ) || + areTablesToUpdated( unresolvedInsertions.getDependentEntityInsertActions(), tables ) || areTablesToUpdated( deletions, tables ) || areTablesToUpdated( collectionUpdates, tables ) || areTablesToUpdated( collectionCreations, tables ) || @@ -242,12 +308,12 @@ public class ActionQueue { * @return True if insertions or deletions are currently queued; false otherwise. */ public boolean areInsertionsOrDeletionsQueued() { - return ( insertions.size() > 0 || deletions.size() > 0 ); + return ( insertions.size() > 0 || ! unresolvedInsertions.isEmpty() || deletions.size() > 0 ); } @SuppressWarnings({ "unchecked" }) - private static boolean areTablesToUpdated(List actions, Set tableSpaces) { - for ( Executable action : (List) actions ) { + private static boolean areTablesToUpdated(Iterable actions, Set tableSpaces) { + for ( Executable action : (Iterable) actions ) { final Serializable[] spaces = action.getPropertySpaces(); for ( Serializable space : spaces ) { if ( tableSpaces.contains( space ) ) { @@ -309,6 +375,7 @@ public class ActionQueue { .append( " collectionCreations=" ).append( collectionCreations ) .append( " collectionRemovals=" ).append( collectionRemovals ) .append( " collectionUpdates=" ).append( collectionUpdates ) + .append( " unresolvedInsertDependencies=" ).append( unresolvedInsertions ) .append( "]" ) .toString(); } @@ -399,6 +466,7 @@ public class ActionQueue { public boolean hasAnyQueuedActions() { return updates.size() > 0 || insertions.size() > 0 || + ! unresolvedInsertions.isEmpty() || deletions.size() > 0 || collectionUpdates.size() > 0 || collectionRemovals.size() > 0 || @@ -427,6 +495,8 @@ public class ActionQueue { public void serialize(ObjectOutputStream oos) throws IOException { LOG.trace( "Serializing action-queue" ); + unresolvedInsertions.serialize( oos ); + int queueSize = insertions.size(); LOG.tracev( "Starting serialization of [{0}] insertions entries", queueSize ); oos.writeInt( queueSize ); @@ -489,6 +559,8 @@ public class ActionQueue { LOG.trace( "Dedeserializing action-queue" ); ActionQueue rtn = new ActionQueue( session ); + rtn.unresolvedInsertions = UnresolvedEntityInsertActions.deserialize( ois, session ); + int queueSize = ois.readInt(); LOG.tracev( "Starting deserialization of [{0}] insertions entries", queueSize ); rtn.insertions = new ArrayList( queueSize ); diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java index 54813bfcad..2d398a08ed 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java @@ -30,6 +30,7 @@ import org.jboss.logging.Logger; import org.hibernate.LockMode; import org.hibernate.NonUniqueObjectException; +import org.hibernate.action.internal.AbstractEntityInsertAction; import org.hibernate.action.internal.EntityIdentityInsertAction; import org.hibernate.action.internal.EntityInsertAction; import org.hibernate.bytecode.instrumentation.internal.FieldInterceptionHelper; @@ -37,7 +38,6 @@ import org.hibernate.bytecode.instrumentation.spi.FieldInterceptor; import org.hibernate.classic.Lifecycle; import org.hibernate.engine.internal.Cascade; import org.hibernate.engine.internal.ForeignKeys; -import org.hibernate.engine.internal.Nullability; import org.hibernate.engine.internal.Versioning; import org.hibernate.engine.spi.CascadingAction; import org.hibernate.engine.spi.EntityEntry; @@ -265,11 +265,6 @@ public abstract class AbstractSaveEventListener extends AbstractReassociateEvent cascadeBeforeSave( source, persister, entity, anything ); - if ( useIdentityColumn && !shouldDelayIdentityInserts ) { - LOG.trace( "Executing insertions" ); - source.getActionQueue().executeInserts(); - } - Object[] values = persister.getPropertyValuesToInsert( entity, getMergeMap( anything ), source ); Type[] types = persister.getPropertyTypes(); @@ -291,56 +286,52 @@ public abstract class AbstractSaveEventListener extends AbstractReassociateEvent source ); - new ForeignKeys.Nullifier( entity, false, useIdentityColumn, source ) - .nullifyTransientReferences( values, types ); - new Nullability( source ).checkNullability( values, persister, false ); - - if ( useIdentityColumn ) { - EntityIdentityInsertAction insert = new EntityIdentityInsertAction( - values, entity, persister, source, shouldDelayIdentityInserts - ); - if ( !shouldDelayIdentityInserts ) { - LOG.debug( "Executing identity-insert immediately" ); - source.getActionQueue().execute( insert ); - id = insert.getGeneratedId(); - key = source.generateEntityKey( id, persister ); - source.getPersistenceContext().checkUniqueness( key, entity ); - } - else { - LOG.debug( "Delaying identity-insert due to no transaction in progress" ); - source.getActionQueue().addAction( insert ); - key = insert.getDelayedEntityKey(); - } - } - - Object version = Versioning.getVersion( values, persister ); - source.getPersistenceContext().addEntity( - entity, - ( persister.isMutable() ? Status.MANAGED : Status.READ_ONLY ), - values, - key, - version, - LockMode.WRITE, - useIdentityColumn, - persister, - isVersionIncrementDisabled(), - false + AbstractEntityInsertAction insert = addInsertAction( + values, id, entity, persister, useIdentityColumn, source, shouldDelayIdentityInserts ); - //source.getPersistenceContext().removeNonExist( new EntityKey( id, persister, source.getEntityMode() ) ); - - if ( !useIdentityColumn ) { - source.getActionQueue().addAction( - new EntityInsertAction( id, values, entity, version, persister, source ) - ); - } + // postpone initializing id in case the insert has non-nullable transient dependencies + // that are not resolved until cascadeAfterSave() is executed cascadeAfterSave( source, persister, entity, anything ); + if ( useIdentityColumn && insert.isEarlyInsert() ) { + if ( ! EntityIdentityInsertAction.class.isInstance( insert ) ) { + throw new IllegalStateException( + "Insert should be using an identity column, but action is of unexpected type: " + + insert.getClass().getName() ); + } + id = ( ( EntityIdentityInsertAction ) insert ).getGeneratedId(); + } markInterceptorDirty( entity, persister, source ); return id; } + private AbstractEntityInsertAction addInsertAction( + Object[] values, + Serializable id, + Object entity, + EntityPersister persister, + boolean useIdentityColumn, + EventSource source, + boolean shouldDelayIdentityInserts) { + if ( useIdentityColumn ) { + EntityIdentityInsertAction insert = new EntityIdentityInsertAction( + values, entity, persister, isVersionIncrementDisabled(), source, shouldDelayIdentityInserts + ); + source.getActionQueue().addAction( insert ); + return insert; + } + else { + Object version = Versioning.getVersion( values, persister ); + EntityInsertAction insert = new EntityInsertAction( + id, values, entity, version, persister, isVersionIncrementDisabled(), source + ); + source.getActionQueue().addAction( insert ); + return insert; + } + } + private void markInterceptorDirty(Object entity, EntityPersister persister, EventSource source) { InstrumentationService instrumentationService = persister.getFactory() .getServiceRegistry() diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java index dca90f115d..3ba3a4ca2c 100755 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java @@ -24,19 +24,14 @@ package org.hibernate.event.internal; import java.io.Serializable; -import java.util.HashSet; -import java.util.Iterator; import java.util.Map; -import java.util.Set; import org.jboss.logging.Logger; import org.hibernate.AssertionFailure; import org.hibernate.HibernateException; import org.hibernate.ObjectDeletedException; -import org.hibernate.PropertyValueException; import org.hibernate.StaleObjectStateException; -import org.hibernate.TransientObjectException; import org.hibernate.WrongClassException; import org.hibernate.bytecode.instrumentation.internal.FieldInterceptionHelper; import org.hibernate.bytecode.instrumentation.spi.FieldInterceptor; @@ -46,7 +41,6 @@ import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SessionImplementor; -import org.hibernate.engine.spi.Status; import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.MergeEvent; import org.hibernate.event.spi.MergeEventListener; @@ -56,7 +50,6 @@ import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; import org.hibernate.service.instrumentation.spi.InstrumentationService; import org.hibernate.type.ForeignKeyDirection; -import org.hibernate.type.Type; import org.hibernate.type.TypeHelper; /** @@ -84,105 +77,10 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme public void onMerge(MergeEvent event) throws HibernateException { EventCache copyCache = new EventCache(); onMerge( event, copyCache ); - // 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, true ); - // 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.entrySet().iterator(); it.hasNext(); ) { - Object transientEntity = ( ( Map.Entry ) it.next() ).getKey(); - String transientEntityName = event.getSession().guessEntityName( transientEntity ); - transientEntityNames.add( transientEntityName ); - LOG.tracev( - "Transient instance could not be processed by merge when checking nullability: {0} [{1}]", - transientEntityName, transientEntity ); - } - if ( isNullabilityCheckedGlobal( event.getSession() ) ) - throw new TransientObjectException( - "one or more objects is an unsaved transient instance - save transient instance(s) before merging: " + - transientEntityNames ); - LOG.trace( "Retry saving transient instances without checking nullability" ); - // failures will be detected later... - retryMergeTransientEntities( event, transientCopyCache, copyCache, false ); - } - } copyCache.clear(); copyCache = null; } - protected EventCache getTransientCopyCache(MergeEvent event, EventCache copyCache) { - EventCache transientCopyCache = new EventCache(); - 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 - if ( LOG.isTraceEnabled() ) { - LOG.tracev( "Transient instance could not be processed by merge: {0} [{1}]", - event.getSession().guessEntityName( copy ), entity ); - } - // merge did not cascade to this entity; it's in copyCache because a - // different entity has a non-nullable reference to this entity; - // this entity should not be put in transientCopyCache, because it was - // not included in the merge; - // if the global setting for checking nullability is false, the non-nullable - // reference to this entity will be detected later - if ( isNullabilityCheckedGlobal( event.getSession() ) ) { - 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.isOperatedOn( 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, - EventCache copyCache, - boolean isNullabilityChecked) { - // 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 ); - mergeTransientEntity( - entity, - copyEntry.getEntityName(), - ( entity == event.getEntity() ? event.getRequestedId() : copyEntry.getId() ), - event.getSession(), - copyCache, - isNullabilityChecked - ); - } - } - /** * Handle the given merge event. * @@ -298,26 +196,7 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme final Object entity = event.getEntity(); final EventSource source = event.getSession(); - final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); - final String entityName = persister.getEntityName(); - - event.setResult( mergeTransientEntity( entity, entityName, event.getRequestedId(), source, copyCache, true ) ); - } - - protected Object mergeTransientEntity(Object entity, String entityName, Serializable requestedId, EventSource source, Map copyCache) { - return mergeTransientEntity( entity, entityName, requestedId, source, copyCache, true ); - } - - private Object mergeTransientEntity( - Object entity, - String entityName, - Serializable requestedId, - EventSource source, - Map copyCache, - boolean isNullabilityChecked) { - - LOG.trace( "Merging transient instance" ); - + final String entityName = event.getEntityName(); final EntityPersister persister = source.getEntityPersister( entityName, entity ); final Serializable id = persister.hasIdentifierProperty() ? @@ -337,70 +216,14 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme super.cascadeBeforeSave(source, persister, entity, copyCache); copyValues(persister, entity, copy, source, copyCache, ForeignKeyDirection.FOREIGN_KEY_FROM_PARENT); - try { - // try saving; check for non-nullable properties that are null or transient entities before saving - saveTransientEntity( copy, entityName, requestedId, source, copyCache, isNullabilityChecked ); - } - catch (PropertyValueException ex) { - String propertyName = ex.getPropertyName(); - Object propertyFromCopy = persister.getPropertyValue( copy, propertyName ); - Object propertyFromEntity = persister.getPropertyValue( entity, propertyName ); - Type propertyType = persister.getPropertyType( propertyName ); - EntityEntry copyEntry = source.getPersistenceContext().getEntry( copy ); - if ( propertyFromCopy == null || - propertyFromEntity == null || - ! propertyType.isEntityType() || - ! copyCache.containsKey( propertyFromEntity ) ) { - if ( LOG.isTraceEnabled() ) { - LOG.trace("Property '" + copyEntry.getEntityName() + "." + propertyName + "' in copy is " - + (propertyFromCopy == null ? "null" : propertyFromCopy)); - LOG.trace("Property '" + copyEntry.getEntityName() + "." + propertyName + "' in original is " - + (propertyFromCopy == null ? "null" : propertyFromCopy)); - LOG.trace("Property '" + copyEntry.getEntityName() + "." + propertyName + "' is" - + (propertyType.isEntityType() ? "" : " not") + " an entity type"); - if (propertyFromEntity != null && !copyCache.containsKey(propertyFromEntity)) { - LOG.tracef( - "Property '%s.%s' is not in copy cache", - copyEntry.getEntityName(), - propertyName - ); - } - } - if ( isNullabilityCheckedGlobal( source ) ) { - throw ex; - } - else { - // retry save w/o checking for non-nullable properties - // (the failure will be detected later) - saveTransientEntity( copy, entityName, requestedId, source, copyCache, false ); - } - } - if ( LOG.isTraceEnabled() && propertyFromEntity != null ) { - if (((EventCache)copyCache).isOperatedOn(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 - } + saveTransientEntity( copy, entityName, event.getRequestedId(), source, copyCache ); // 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); - return copy; - - } - - private boolean isNullabilityCheckedGlobal(EventSource source) { - return source.getFactory().getSettings().isCheckNullability(); + event.setResult( copy ); } private void saveTransientEntity( @@ -408,27 +231,18 @@ public class DefaultMergeEventListener extends AbstractSaveEventListener impleme String entityName, Serializable requestedId, EventSource source, - Map copyCache, - boolean isNullabilityChecked) { - - boolean isNullabilityCheckedOrig = - source.getFactory().getSettings().isCheckNullability(); - try { - source.getFactory().getSettings().setCheckNullability( isNullabilityChecked ); - //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( entity, entityName, copyCache, source, false ); - } - else { - saveWithRequestedId( entity, requestedId, entityName, copyCache, source ); - } + Map copyCache) { + //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( entity, entityName, copyCache, source, false ); } - finally { - source.getFactory().getSettings().setCheckNullability( isNullabilityCheckedOrig ); + else { + saveWithRequestedId( entity, requestedId, entityName, copyCache, source ); } } + protected void entityIsDetached(MergeEvent event, Map copyCache) { LOG.trace( "Merging detached instance" ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index 2cebe42623..d6f3da6b05 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -606,6 +606,17 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc } } + private void checkDelayedActionStatusBeforeOperation() { + if ( persistenceContext.getCascadeLevel() == 0 && actionQueue.hasUnresolvedEntityInsertActions() ) { + throw new IllegalStateException( "There are delayed insert actions before operation as cascade level 0." ); + } + } + + private void checkDelayedActionStatusAfterOperation() { + if ( persistenceContext.getCascadeLevel() == 0 ) { + actionQueue.checkNoUnresolvedEntityInsertActions(); + } + } // saveOrUpdate() operations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -620,9 +631,11 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc private void fireSaveOrUpdate(SaveOrUpdateEvent event) { errorIfClosed(); checkTransactionSynchStatus(); + checkDelayedActionStatusBeforeOperation(); for ( SaveOrUpdateEventListener listener : listeners( EventType.SAVE_UPDATE ) ) { listener.onSaveOrUpdate( event ); } + checkDelayedActionStatusAfterOperation(); } private Iterable listeners(EventType type) { @@ -647,9 +660,11 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc private Serializable fireSave(SaveOrUpdateEvent event) { errorIfClosed(); checkTransactionSynchStatus(); + checkDelayedActionStatusBeforeOperation(); for ( SaveOrUpdateEventListener listener : listeners( EventType.SAVE ) ) { listener.onSaveOrUpdate( event ); } + checkDelayedActionStatusAfterOperation(); return event.getResultId(); } @@ -667,9 +682,11 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc private void fireUpdate(SaveOrUpdateEvent event) { errorIfClosed(); checkTransactionSynchStatus(); + checkDelayedActionStatusBeforeOperation(); for ( SaveOrUpdateEventListener listener : listeners( EventType.UPDATE ) ) { listener.onSaveOrUpdate( event ); } + checkDelayedActionStatusAfterOperation(); } @@ -730,9 +747,11 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc private void firePersist(PersistEvent event) { errorIfClosed(); checkTransactionSynchStatus(); + checkDelayedActionStatusBeforeOperation(); for ( PersistEventListener listener : listeners( EventType.PERSIST ) ) { listener.onPersist( event ); } + checkDelayedActionStatusAfterOperation(); } @@ -763,9 +782,11 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc private void firePersistOnFlush(PersistEvent event) { errorIfClosed(); checkTransactionSynchStatus(); + checkDelayedActionStatusBeforeOperation(); for ( PersistEventListener listener : listeners( EventType.PERSIST_ONFLUSH ) ) { listener.onPersist( event ); } + checkDelayedActionStatusAfterOperation(); } @@ -786,9 +807,11 @@ public final class SessionImpl extends AbstractSessionImpl implements EventSourc private Object fireMerge(MergeEvent event) { errorIfClosed(); checkTransactionSynchStatus(); + checkDelayedActionStatusBeforeOperation(); for ( MergeEventListener listener : listeners( EventType.MERGE ) ) { listener.onMerge( event ); } + checkDelayedActionStatusAfterOperation(); return event.getResult(); } diff --git a/hibernate-core/src/matrix/java/org/hibernate/test/annotations/cascade/circle/identity/CascadeCircleIdentityIdTest.java b/hibernate-core/src/matrix/java/org/hibernate/test/annotations/cascade/circle/identity/CascadeCircleIdentityIdTest.java index 70297c037b..3c493f7465 100644 --- a/hibernate-core/src/matrix/java/org/hibernate/test/annotations/cascade/circle/identity/CascadeCircleIdentityIdTest.java +++ b/hibernate-core/src/matrix/java/org/hibernate/test/annotations/cascade/circle/identity/CascadeCircleIdentityIdTest.java @@ -30,12 +30,13 @@ import org.hibernate.testing.DialectCheck; import org.hibernate.testing.DialectChecks; import org.hibernate.testing.FailureExpected; import org.hibernate.testing.RequiresDialectFeature; +import org.hibernate.testing.TestForIssue; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; @RequiresDialectFeature(DialectChecks.SupportsIdentityColumns.class) public class CascadeCircleIdentityIdTest extends BaseCoreFunctionalTestCase { @Test - @FailureExpected( jiraKey = "HHH-5472" ) + @TestForIssue( jiraKey = "HHH-5472" ) public void testCascade() { A a = new A(); B b = new B(); diff --git a/hibernate-core/src/matrix/java/org/hibernate/test/annotations/cascade/circle/sequence/CascadeCircleSequenceIdTest.java b/hibernate-core/src/matrix/java/org/hibernate/test/annotations/cascade/circle/sequence/CascadeCircleSequenceIdTest.java index 42968c3eb2..2ff25e635d 100644 --- a/hibernate-core/src/matrix/java/org/hibernate/test/annotations/cascade/circle/sequence/CascadeCircleSequenceIdTest.java +++ b/hibernate-core/src/matrix/java/org/hibernate/test/annotations/cascade/circle/sequence/CascadeCircleSequenceIdTest.java @@ -27,6 +27,7 @@ import org.hibernate.Session; import org.hibernate.testing.DialectChecks; import org.hibernate.testing.FailureExpected; import org.hibernate.testing.RequiresDialectFeature; +import org.hibernate.testing.TestForIssue; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; import org.junit.Test; @@ -34,11 +35,11 @@ import org.junit.Test; @RequiresDialectFeature(DialectChecks.SupportsSequences.class) public class CascadeCircleSequenceIdTest extends BaseCoreFunctionalTestCase { @Test - @FailureExpected( jiraKey = "HHH-5472" ) + @TestForIssue( jiraKey = "HHH-5472" ) public void testCascade() { A a = new A(); - org.hibernate.test.annotations.cascade.circle.sequence.B b = new org.hibernate.test.annotations.cascade.circle.sequence.B(); - org.hibernate.test.annotations.cascade.circle.sequence.C c = new org.hibernate.test.annotations.cascade.circle.sequence.C(); + B b = new B(); + C c = new C(); D d = new D(); E e = new E(); F f = new F(); @@ -86,8 +87,8 @@ public class CascadeCircleSequenceIdTest extends BaseCoreFunctionalTestCase { protected Class[] getAnnotatedClasses() { return new Class[]{ A.class, - org.hibernate.test.annotations.cascade.circle.sequence.B.class, - org.hibernate.test.annotations.cascade.circle.sequence.C.class, + B.class, + C.class, D.class, E.class, F.class, diff --git a/hibernate-core/src/matrix/java/org/hibernate/test/cascade/circle/MultiPathCircleCascade.hbm.xml b/hibernate-core/src/matrix/java/org/hibernate/test/cascade/circle/MultiPathCircleCascade.hbm.xml index ac1f63352c..f18cc8e2e0 100644 --- a/hibernate-core/src/matrix/java/org/hibernate/test/cascade/circle/MultiPathCircleCascade.hbm.xml +++ b/hibernate-core/src/matrix/java/org/hibernate/test/cascade/circle/MultiPathCircleCascade.hbm.xml @@ -9,7 +9,7 @@ - + @@ -21,7 +21,7 @@ - + @@ -37,14 +37,14 @@ column="pickupNodeID" unique="true" not-null="true" - cascade="merge,refresh" + cascade="merge,save-update,refresh" lazy="false"/> @@ -54,12 +54,12 @@ - + - + @@ -75,7 +75,7 @@ column="tourID" unique="false" not-null="false" - cascade="merge,refresh" + cascade="merge,save-update,refresh" lazy="false"/> diff --git a/hibernate-core/src/matrix/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeDelayedInsert.hbm.xml b/hibernate-core/src/matrix/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeDelayedInsert.hbm.xml index 985fd92891..252bc610d8 100644 --- a/hibernate-core/src/matrix/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeDelayedInsert.hbm.xml +++ b/hibernate-core/src/matrix/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeDelayedInsert.hbm.xml @@ -9,7 +9,7 @@ - + @@ -21,7 +21,7 @@ - + @@ -37,14 +37,14 @@ column="pickupNodeID" unique="true" not-null="true" - cascade="merge,refresh" + cascade="save-update,merge,refresh" lazy="false"/> @@ -54,12 +54,12 @@ - + - + @@ -75,7 +75,7 @@ column="tourID" unique="false" not-null="false" - cascade="merge,refresh" + cascade="save-update,merge,refresh" lazy="false"/> diff --git a/hibernate-core/src/matrix/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeTest.java b/hibernate-core/src/matrix/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeTest.java index a310c905a8..6288c66de4 100644 --- a/hibernate-core/src/matrix/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeTest.java +++ b/hibernate-core/src/matrix/java/org/hibernate/test/cascade/circle/MultiPathCircleCascadeTest.java @@ -34,10 +34,6 @@ import org.hibernate.TransientObjectException; import org.hibernate.cfg.Configuration; import org.hibernate.cfg.Environment; import org.hibernate.engine.spi.SessionImplementor; -import org.hibernate.id.IdentifierGenerator; -import org.hibernate.id.IncrementGenerator; -import org.hibernate.id.SequenceGenerator; -import org.hibernate.testing.SkipLog; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; import static org.junit.Assert.assertEquals; @@ -58,17 +54,44 @@ import static org.junit.Assert.fail; * | <- -> | * -- (1 : N) -- (delivery) -- * - * Arrows indicate the direction of cascade-merge. + * Arrows indicate the direction of cascade-merge, cascade-save, and cascade-save-or-update * * 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. + * This tests that cascades are done properly from each entity. * * @author Pavol Zibrita, Gail Badner */ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { + private static interface EntityOperation { + Object doEntityOperation(Object entity, Session s); + } + private static EntityOperation MERGE_OPERATION = + new EntityOperation() { + @Override + public Object doEntityOperation(Object entity, Session s) { + return s.merge( entity ); + } + }; + private static EntityOperation SAVE_OPERATION = + new EntityOperation() { + @Override + public Object doEntityOperation(Object entity, Session s) { + s.save( entity ); + return entity; + } + }; + private static EntityOperation SAVE_UPDATE_OPERATION = + new EntityOperation() { + @Override + public Object doEntityOperation(Object entity, Session s) { + s.saveOrUpdate( entity ); + return entity; + } + }; + @Override public void configure(Configuration cfg) { cfg.setProperty( Environment.GENERATE_STATISTICS, "true" ); @@ -82,30 +105,19 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { }; } - 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" ); - } - @Test public void testMergeEntityWithNonNullableTransientEntity() { - // Skip if CHECK_NULLABILITY is false and route ID is a sequence or incrment generator (see HHH-6744) - IdentifierGenerator routeIdentifierGenerator = sessionFactory().getEntityPersister( Route.class.getName() ).getIdentifierGenerator(); - if ( ! sessionFactory().getSettings().isCheckNullability() && - ( SequenceGenerator.class.isInstance( routeIdentifierGenerator) || - IncrementGenerator.class.isInstance( routeIdentifierGenerator ) ) - ) { - SkipLog.reportSkip( - "delayed-insert without checking nullability", - "delayed-insert without checking nullability is known to fail when dirty-checking; see HHH-6744" - ); - return; - } - + testEntityWithNonNullableTransientEntity( MERGE_OPERATION ); + } + @Test + public void testSaveEntityWithNonNullableTransientEntity() { + testEntityWithNonNullableTransientEntity( SAVE_OPERATION ); + } + @Test + public void testSaveUpdateEntityWithNonNullableTransientEntity() { + testEntityWithNonNullableTransientEntity( SAVE_UPDATE_OPERATION ); + } + private void testEntityWithNonNullableTransientEntity(EntityOperation operation) { Route route = getUpdatedDetachedEntity(); @@ -121,7 +133,7 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { s.beginTransaction(); try { - s.merge( node ); + operation.doEntityOperation( node, s ); s.getTransaction().commit(); fail( "should have thrown an exception" ); } @@ -135,11 +147,23 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { finally { s.getTransaction().rollback(); s.close(); + cleanup(); } } @Test public void testMergeEntityWithNonNullableEntityNull() { + testEntityWithNonNullableEntityNull( MERGE_OPERATION ); + } + @Test + public void testSaveEntityWithNonNullableEntityNull() { + testEntityWithNonNullableEntityNull( SAVE_OPERATION ); + } + @Test + public void testSaveUpdateEntityWithNonNullableEntityNull() { + testEntityWithNonNullableEntityNull( SAVE_UPDATE_OPERATION ); + } + private void testEntityWithNonNullableEntityNull(EntityOperation operation) { Route route = getUpdatedDetachedEntity(); Node node = (Node) route.getNodes().iterator().next(); @@ -150,7 +174,7 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { s.beginTransaction(); try { - s.merge( node ); + operation.doEntityOperation( node, s ); s.getTransaction().commit(); fail( "should have thrown an exception" ); } @@ -164,11 +188,23 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { finally { s.getTransaction().rollback(); s.close(); + cleanup(); } } @Test public void testMergeEntityWithNonNullablePropSetToNull() { + testEntityWithNonNullablePropSetToNull( MERGE_OPERATION ); + } + @Test + public void testSaveEntityWithNonNullablePropSetToNull() { + testEntityWithNonNullablePropSetToNull( SAVE_OPERATION ); + } + @Test + public void testSaveUpdateEntityWithNonNullablePropSetToNull() { + testEntityWithNonNullablePropSetToNull( SAVE_UPDATE_OPERATION ); + } + private void testEntityWithNonNullablePropSetToNull(EntityOperation operation) { Route route = getUpdatedDetachedEntity(); Node node = (Node) route.getNodes().iterator().next(); node.setName( null ); @@ -177,7 +213,7 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { s.beginTransaction(); try { - s.merge( route ); + operation.doEntityOperation( route, s ); s.getTransaction().commit(); fail( "should have thrown an exception" ); } @@ -191,11 +227,20 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { finally { s.getTransaction().rollback(); s.close(); + cleanup(); } } @Test public void testMergeRoute() { + testRoute( MERGE_OPERATION ); + } + // skip SAVE_OPERATION since Route is not transient + @Test + public void testSaveUpdateRoute() { + testRoute( SAVE_UPDATE_OPERATION ); + } + private void testRoute(EntityOperation operation) { Route route = getUpdatedDetachedEntity(); @@ -204,7 +249,7 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { Session s = openSession(); s.beginTransaction(); - s.merge( route ); + operation.doEntityOperation( route, s ); s.getTransaction().commit(); s.close(); @@ -218,10 +263,23 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { checkResults( route, true ); s.getTransaction().commit(); s.close(); + + cleanup(); } @Test public void testMergePickupNode() { + testPickupNode( MERGE_OPERATION ); + } + @Test + public void testSavePickupNode() { + testPickupNode( SAVE_OPERATION ); + } + @Test + public void testSaveUpdatePickupNode() { + testPickupNode( SAVE_UPDATE_OPERATION ); + } + private void testPickupNode(EntityOperation operation) { Route route = getUpdatedDetachedEntity(); @@ -242,7 +300,7 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { pickupNode = node; } - pickupNode = (Node) s.merge( pickupNode ); + pickupNode = (Node) operation.doEntityOperation( pickupNode, s ); s.getTransaction().commit(); s.close(); @@ -256,10 +314,23 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { checkResults( route, false ); s.getTransaction().commit(); s.close(); + + cleanup(); } @Test public void testMergeDeliveryNode() { + testDeliveryNode( MERGE_OPERATION ); + } + @Test + public void testSaveDeliveryNode() { + testDeliveryNode( SAVE_OPERATION ); + } + @Test + public void testSaveUpdateDeliveryNode() { + testDeliveryNode( SAVE_UPDATE_OPERATION ); + } + private void testDeliveryNode(EntityOperation operation) { Route route = getUpdatedDetachedEntity(); @@ -280,7 +351,7 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { deliveryNode = node; } - deliveryNode = (Node) s.merge( deliveryNode ); + deliveryNode = (Node) operation.doEntityOperation( deliveryNode, s ); s.getTransaction().commit(); s.close(); @@ -294,10 +365,23 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { checkResults( route, false ); s.getTransaction().commit(); s.close(); + + cleanup(); } @Test public void testMergeTour() { + testTour( MERGE_OPERATION ); + } + @Test + public void testSaveTour() { + testTour( SAVE_OPERATION ); + } + @Test + public void testSaveUpdateTour() { + testTour( SAVE_UPDATE_OPERATION ); + } + private void testTour(EntityOperation operation) { Route route = getUpdatedDetachedEntity(); @@ -306,7 +390,7 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { Session s = openSession(); s.beginTransaction(); - Tour tour = (Tour) s.merge( ((Node) route.getNodes().toArray()[0]).getTour() ); + Tour tour = (Tour) operation.doEntityOperation( ((Node) route.getNodes().toArray()[0]).getTour(), s ); s.getTransaction().commit(); s.close(); @@ -320,10 +404,23 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { checkResults( route, false ); s.getTransaction().commit(); s.close(); + + cleanup(); } @Test public void testMergeTransport() { + testTransport( MERGE_OPERATION ); + } + @Test + public void testSaveTransport() { + testTransport( SAVE_OPERATION ); + } + @Test + public void testSaveUpdateTransport() { + testTransport( SAVE_UPDATE_OPERATION ); + } + private void testTransport(EntityOperation operation) { Route route = getUpdatedDetachedEntity(); @@ -341,7 +438,7 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { transport = (Transport) node.getDeliveryTransports().toArray()[0]; } - transport = (Transport) s.merge( transport ); + transport = (Transport) operation.doEntityOperation( transport, s ); s.getTransaction().commit(); s.close(); @@ -355,6 +452,8 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { checkResults( route, false ); s.getTransaction().commit(); s.close(); + + cleanup(); } private Route getUpdatedDetachedEntity() { @@ -407,8 +506,19 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { return route; } + private void cleanup() { + 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.getTransaction().commit(); + s.close(); + } + private void checkResults(Route route, boolean isRouteUpdated) { - // since merge is not cascaded to route, this method needs to + // since no cascaded to route, this method needs to // know whether route is expected to be updated if ( isRouteUpdated ) { assertEquals( "new routeA", route.getName() ); @@ -465,6 +575,17 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { @Test public void testMergeData3Nodes() { + testData3Nodes( MERGE_OPERATION ); + } + @Test + public void testSaveData3Nodes() { + testData3Nodes( SAVE_OPERATION ); + } + @Test + public void testSaveUpdateData3Nodes() { + testData3Nodes( SAVE_UPDATE_OPERATION ); + } + private void testData3Nodes(EntityOperation operation) { Session s = openSession(); s.beginTransaction(); @@ -481,7 +602,7 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { s = openSession(); s.beginTransaction(); - route = (Route) s.get( Route.class, new Long( 1 ) ); + route = (Route) s.get( Route.class, route.getRouteID() ); //System.out.println(route); route.setName( "new routA" ); @@ -537,28 +658,30 @@ public class MultiPathCircleCascadeTest extends BaseCoreFunctionalTestCase { transport2.setDeliveryNode( node3 ); transport2.setTransientField( "bbbbbbbbbbbbb" ); - Route mergedRoute = (Route) s.merge( route ); + operation.doEntityOperation( route, s ); s.getTransaction().commit(); s.close(); assertInsertCount( 6 ); assertUpdateCount( 1 ); + + cleanup(); } protected void checkExceptionFromNullValueForNonNullable( Exception ex, boolean checkNullability, boolean isNullValue ) { - if ( checkNullability ) { - if ( isNullValue ) { + if ( isNullValue ) { + if ( checkNullability ) { assertTrue( ex instanceof PropertyValueException ); } else { - assertTrue( ex instanceof TransientObjectException ); + assertTrue( ex instanceof JDBCException ); } } else { - assertTrue( ex instanceof JDBCException ); + assertTrue( ex instanceof TransientObjectException ); } } diff --git a/hibernate-core/src/matrix/java/org/hibernate/test/nonflushedchanges/CreateTest.java b/hibernate-core/src/matrix/java/org/hibernate/test/nonflushedchanges/CreateTest.java index d9b336d9a6..6285e88f86 100644 --- a/hibernate-core/src/matrix/java/org/hibernate/test/nonflushedchanges/CreateTest.java +++ b/hibernate-core/src/matrix/java/org/hibernate/test/nonflushedchanges/CreateTest.java @@ -125,7 +125,8 @@ public class CreateTest extends AbstractOperationTestCase { root.addChild( child ); s = applyNonFlushedChangesToNewSessionCloseOldSession( s ); s.persist( root ); - applyNonFlushedChangesToNewSessionCloseOldSession( s ); + s = applyNonFlushedChangesToNewSessionCloseOldSession( s ); + root = ( NumberedNode ) getOldToNewEntityRefMap().get( root ); TestingJtaBootstrap.INSTANCE.getTransactionManager().commit(); assertInsertCount( 2 ); @@ -218,6 +219,7 @@ public class CreateTest extends AbstractOperationTestCase { dupe = ( NumberedNode ) getOldToNewEntityRefMap().get( dupe ); s.persist( dupe ); applyNonFlushedChangesToNewSessionCloseOldSession( s ); + dupe = ( NumberedNode ) getOldToNewEntityRefMap().get( dupe ); TestingJtaBootstrap.INSTANCE.getTransactionManager().commit(); TestingJtaBootstrap.INSTANCE.getTransactionManager().begin(); @@ -225,6 +227,7 @@ public class CreateTest extends AbstractOperationTestCase { s = applyNonFlushedChangesToNewSessionCloseOldSession( s ); try { s.persist( dupe ); + s.flush(); assertFalse( true ); } catch ( PersistentObjectException poe ) { diff --git a/hibernate-core/src/matrix/java/org/hibernate/test/ops/SaveOrUpdateTest.java b/hibernate-core/src/matrix/java/org/hibernate/test/ops/SaveOrUpdateTest.java index 5308973ba3..b5abe0afdc 100755 --- a/hibernate-core/src/matrix/java/org/hibernate/test/ops/SaveOrUpdateTest.java +++ b/hibernate-core/src/matrix/java/org/hibernate/test/ops/SaveOrUpdateTest.java @@ -486,6 +486,44 @@ public class SaveOrUpdateTest extends BaseCoreFunctionalTestCase { s.close(); } + @Test + public void testSavePersistentEntityWithUpdate() { + clearCounts(); + + Session s = openSession(); + Transaction tx = s.beginTransaction(); + NumberedNode root = new NumberedNode( "root" ); + root.setName( "a name" ); + s.saveOrUpdate( root ); + tx.commit(); + s.close(); + + assertInsertCount( 1 ); + assertUpdateCount( 0 ); + clearCounts(); + + s = openSession(); + tx = s.beginTransaction(); + root = ( NumberedNode ) s.get( NumberedNode.class, root.getId() ); + assertEquals( "a name", root.getName() ); + root.setName( "a new name" ); + s.save( root ); + tx.commit(); + s.close(); + + assertInsertCount( 0 ); + assertUpdateCount( 1 ); + clearCounts(); + + s = openSession(); + tx = s.beginTransaction(); + root = ( NumberedNode ) s.get( NumberedNode.class, root.getId() ); + assertEquals( "a new name", root.getName() ); + s.delete( root ); + tx.commit(); + s.close(); + } + private void clearCounts() { sessionFactory().getStatistics().clear(); }