HHH-5472 : Delay saving an entity if it does not cascade the save to non-nullable transient entities

This commit is contained in:
Gail Badner 2011-12-20 05:46:55 -08:00
parent 81ee788466
commit e11e9631c7
17 changed files with 1111 additions and 365 deletions

View File

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

View File

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

View File

@ -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 );
}

View File

@ -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<AbstractEntityInsertAction,NonNullableTransientDependencies> dependenciesByAction =
new IdentityHashMap<AbstractEntityInsertAction,NonNullableTransientDependencies>( INIT_LIST_SIZE );
private final Map<Object,Set<AbstractEntityInsertAction>> dependentActionsByTransientEntity =
new IdentityHashMap<Object,Set<AbstractEntityInsertAction>>( 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<AbstractEntityInsertAction> 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<AbstractEntityInsertAction> 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<AbstractEntityInsertAction> 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<AbstractEntityInsertAction> 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<AbstractEntityInsertAction> 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<Object,Set<AbstractEntityInsertAction>> entry : dependentActionsByTransientEntity.entrySet() ) {
if ( firstTransientEntity ) {
firstTransientEntity = false;
}
else {
sb.append( '\n' );
}
Object transientEntity = entry.getKey();
Set<String> propertyPaths = new TreeSet<String>();
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<AbstractEntityInsertAction,NonNullableTransientDependencies> 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;
}
}

View File

@ -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 <tt>assumed</tt> 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
);
}
}
}
}
}

View File

@ -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<Object,Set<String>> propertyPathsByTransientEntity =
new IdentityHashMap<Object,Set<String>>();
/* package-protected */
void add(String entityName, String propertyName, Object transientEntity) {
Set<String> propertyPaths = propertyPathsByTransientEntity.get( transientEntity );
if ( propertyPaths == null ) {
propertyPaths = new HashSet<String>();
propertyPathsByTransientEntity.put( transientEntity, propertyPaths );
}
StringBuilder sb = new StringBuilder( entityName.length() + propertyName.length() + 1 )
.append( entityName )
.append( '.' )
.append( propertyName );
propertyPaths.add( sb.toString() );
}
public Iterable<Object> getNonNullableTransientEntities() {
return propertyPathsByTransientEntity.keySet();
}
public Iterable<String> 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<Object,Set<String>> entry : propertyPathsByTransientEntity.entrySet() ) {
sb.append( "transientEntityName=" ).append( session.bestGuessEntityName( entry.getKey() ) );
sb.append( " requiredBy=" ).append( entry.getValue() );
}
sb.append( ']' );
return sb.toString();
}
}

View File

@ -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<EntityDeleteAction> 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<AbstractEntityInsertAction>( INIT_QUEUE_LIST_SIZE );
deletions = new ArrayList<EntityDeleteAction>( 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<Executable>) actions ) {
private static boolean areTablesToUpdated(Iterable actions, Set tableSpaces) {
for ( Executable action : (Iterable<Executable>) 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<Executable>( queueSize );

View File

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

View File

@ -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" );

View File

@ -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 <T> Iterable<T> listeners(EventType<T> 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();
}

View File

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

View File

@ -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,

View File

@ -9,7 +9,7 @@
<property name="name" type="string" not-null="true"/>
<set name="nodes" inverse="true" cascade="persist,merge,refresh">
<set name="nodes" inverse="true" cascade="persist,merge,save-update,refresh">
<key column="routeID"/>
<one-to-many class="Node"/>
</set>
@ -21,7 +21,7 @@
<property name="name" type="string" not-null="true"/>
<set name="nodes" inverse="true" lazy="true" cascade="merge,refresh">
<set name="nodes" inverse="true" lazy="true" cascade="merge,save-update,refresh">
<key column="tourID"/>
<one-to-many class="Node"/>
</set>
@ -37,14 +37,14 @@
column="pickupNodeID"
unique="true"
not-null="true"
cascade="merge,refresh"
cascade="merge,save-update,refresh"
lazy="false"/>
<many-to-one name="deliveryNode"
column="deliveryNodeID"
unique="true"
not-null="true"
cascade="merge,refresh"
cascade="merge,save-update,refresh"
lazy="false"/>
</class>
@ -54,12 +54,12 @@
<property name="name" type="string" not-null="true"/>
<set name="deliveryTransports" inverse="true" lazy="true" cascade="merge,refresh">
<set name="deliveryTransports" inverse="true" lazy="true" cascade="merge,save-update,refresh">
<key column="deliveryNodeID"/>
<one-to-many class="Transport"/>
</set>
<set name="pickupTransports" inverse="true" lazy="true" cascade="merge,refresh">
<set name="pickupTransports" inverse="true" lazy="true" cascade="merge,save-update,refresh">
<key column="pickupNodeID"/>
<one-to-many class="Transport"/>
</set>
@ -75,7 +75,7 @@
column="tourID"
unique="false"
not-null="false"
cascade="merge,refresh"
cascade="merge,save-update,refresh"
lazy="false"/>
</class>

View File

@ -9,7 +9,7 @@
<property name="name" type="string" not-null="true"/>
<set name="nodes" inverse="true" cascade="persist,merge,refresh">
<set name="nodes" inverse="true" cascade="persist,save-update,merge,refresh">
<key column="routeID"/>
<one-to-many class="Node"/>
</set>
@ -21,7 +21,7 @@
<property name="name" type="string" not-null="true"/>
<set name="nodes" inverse="true" lazy="true" cascade="merge,refresh">
<set name="nodes" inverse="true" lazy="true" cascade="save-update,merge,refresh">
<key column="tourID"/>
<one-to-many class="Node"/>
</set>
@ -37,14 +37,14 @@
column="pickupNodeID"
unique="true"
not-null="true"
cascade="merge,refresh"
cascade="save-update,merge,refresh"
lazy="false"/>
<many-to-one name="deliveryNode"
column="deliveryNodeID"
unique="true"
not-null="true"
cascade="merge,refresh"
cascade="save-update,merge,refresh"
lazy="false"/>
</class>
@ -54,12 +54,12 @@
<property name="name" type="string" not-null="true"/>
<set name="deliveryTransports" inverse="true" lazy="true" cascade="merge,refresh">
<set name="deliveryTransports" inverse="true" lazy="true" cascade="save-update,merge,refresh">
<key column="deliveryNodeID"/>
<one-to-many class="Transport"/>
</set>
<set name="pickupTransports" inverse="true" lazy="true" cascade="merge,refresh">
<set name="pickupTransports" inverse="true" lazy="true" cascade="save-update,merge,refresh">
<key column="pickupNodeID"/>
<one-to-many class="Transport"/>
</set>
@ -75,7 +75,7 @@
column="tourID"
unique="false"
not-null="false"
cascade="merge,refresh"
cascade="save-update,merge,refresh"
lazy="false"/>
</class>

View File

@ -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
* <p/>
* 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 );
}
}

View File

@ -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 ) {

View File

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