HHH-5472 : Delay saving an entity if it does not cascade the save to non-nullable transient entities
This commit is contained in:
parent
81ee788466
commit
e11e9631c7
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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 );
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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" );
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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 );
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 ) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue