/* * Hibernate, Relational Persistence for Idiomatic Java * * Copyright (c) 2010, 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 */ /** * Modified by McKesson Corporation and/or one of its subsidiaries. * Modifications copyright �2010 McKesson Corporation and/or one of its subsidiaries. All rights reserved. * * Modifications made September 16, 2010; October 5, 2010; December 14, 2010: * Added tryToCoalesceUpdateIntoInsert() to allow "true coalesce" of new entities into a session. Added coalesceExtendedPersistenceQueue() to clean up the ActionQueue and remove spurious DML. Added prepareToCoalesceExtendedPersistenceQueue() to support the coalesce functionality. Added methods to expose the opjects that are queued for insertion, deletion and update. Also added a configurable property "useTrueCoalesce" to turn this new functionality on or off. This property is set in the ActionQueue constructor. See Javadocs on methods below for more details. */ package org.hibernate.engine; import org.hibernate.AssertionFailure; import org.hibernate.HibernateException; import org.hibernate.action.*; import org.hibernate.cache.CacheException; import org.hibernate.type.Type; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; import java.util.*; /** * Responsible for maintaining the queue of actions related to events. * </p> * The ActionQueue holds the DML operations queued as part of a session's * transactional-write-behind semantics. DML operations are queued here * until a flush forces them to be executed against the database. * * @author Steve Ebersole */ public class ActionQueue { private static final Logger log = LoggerFactory.getLogger( ActionQueue.class ); private static final int INIT_QUEUE_LIST_SIZE = 5; private SessionImplementor session; // Object insertions, updates, and deletions have list semantics because // they must happen in the right order so as to respect referential // integrity private ArrayList insertions; private ArrayList deletions; private ArrayList updates; // Actually the semantics of the next three are really "Bag" // Note that, unlike objects, collection insertions, updates, // deletions are not really remembered between flushes. We // just re-use the same Lists for convenience. private ArrayList collectionCreations; private ArrayList collectionUpdates; private ArrayList collectionRemovals; private AfterTransactionCompletionProcessQueue afterTransactionProcesses; private BeforeTransactionCompletionProcessQueue beforeTransactionProcesses; /** * Constructs an action queue bound to the given session. * * @param session The session "owning" this queue. */ public ActionQueue(SessionImplementor session) { this.session = session; init(); //McKesson extension useTrueCoalesce = Boolean.parseBoolean(session.getFactory().getProperties().getProperty("hibernate.extension.useTrueCoalesce")); } private void init() { insertions = new ArrayList( INIT_QUEUE_LIST_SIZE ); deletions = new ArrayList( INIT_QUEUE_LIST_SIZE ); updates = new ArrayList( INIT_QUEUE_LIST_SIZE ); collectionCreations = new ArrayList( INIT_QUEUE_LIST_SIZE ); collectionRemovals = new ArrayList( INIT_QUEUE_LIST_SIZE ); collectionUpdates = new ArrayList( INIT_QUEUE_LIST_SIZE ); afterTransactionProcesses = new AfterTransactionCompletionProcessQueue( session ); beforeTransactionProcesses = new BeforeTransactionCompletionProcessQueue( session ); } public void clear() { updates.clear(); insertions.clear(); deletions.clear(); collectionCreations.clear(); collectionRemovals.clear(); collectionUpdates.clear(); } // begin McKesson customizations /** * Boolean indicating whether or not to use coalesce */ private final boolean useTrueCoalesce; /** * Inserted objects tracked for coalesce */ private ArrayList insertionsToRemove; /** * Updated objects tracked for coalesce */ private ArrayList updatesToRemove; /** * Deleted objects tracked for coalesce */ private ArrayList deletionsToRemove; /** * Added by McKesson to allow "true coalesce" of new entities into a session. What this means is that instead * of an INSERT/UPDATE pair being flushed, we will just have one INSERT with all the needfuls. The specific use case * was this: * <pre> * pojo = new MyPojo() * pojo.setFoo("foo value") * * hibernateSession.persist(pojo) // or merge * * pojo.setBar("bar value") * * hibernateSession.flush() // NO!!! This results in an INSERT *and* an UPDATE!!!! see http://www.delicious.com/sfraser/coalesce * </pre> * * So in the previous pseudo code, the undesired behavior is that Hibernate queues up an INSERT that represents the * state of the pojo at persist or merge time, and THEN an UPDATE syncing up the dirty starte of the managed * pojo. In this case we would see bar = "bar value" in the UPDATE. * * This is very problematic for some applications. With the increased usage of "extended persistence context" such * as in JBoss Seam and Spring Web Flow, this problem should become more of an issue. For example, what if your * database has NOT NULL on "bar" above? The first INSERT would fail! Or what if you have DML triggers and having * an INSERT/UPDATE pair is a problem? Not much you can do... * * This patch scans the ActionQueue and looks for queued up INSERT's on the passed in entity. If it finds a match, * it simply overlays the INSERT values with the current dirty values, and returns true. The caller (in this case * DefaultFlushEntityEventListener.scheduleUpdate) can then decide what to do. In this case the caller will * decide not to queue up an UPDATE action. * * @param p_instance The entity that is being updated * @param p_updateState The Update state * @return Whether or not update state was merged into an already existing EntityInsertAction * @see EntityInsertAction#setState(Object[]) * @see org.hibernate.event.def.DefaultFlushEntityEventListener#scheduleUpdate(org.hibernate.event.FlushEntityEvent) * */ public boolean tryToCoalesceUpdateIntoInsert(final Object p_instance, final Object[] p_updateState) { //if coalesce is disabled, don't do anything //and inform the caller that nothing is to be done if(!useTrueCoalesce){ return false; } final Iterator iterator = insertions.iterator(); while (iterator.hasNext()) { final EntityInsertAction insertAction = (EntityInsertAction)iterator.next(); // if we find an INSERT for the same entity that is the passed in UPDATE, // we are going to monkey patch the insert: if( insertAction.getInstance() == p_instance ) { insertAction.setState(p_updateState); return true; } } return false; } /** * This method cleans up the actionQueue so that when * the model is flushed to the database, spurious updates, inserts, * and deletes are eliminated. The following cases are currently handled: * * * 1) When an insert and a delete is queued on the same object, but actions are removed from the queue * 2) When an update and a delete are queued on the same object, the update is removed * */ public void coalesceExtendedPersistenceQueue() { //if coalesce is disabled, don't do anything if(!useTrueCoalesce){ return; } Iterator iterator = insertionsToRemove.iterator(); while(iterator.hasNext()){ insertions.remove(iterator.next()); } iterator = deletionsToRemove.iterator(); while(iterator.hasNext()){ deletions.remove(iterator.next()); } iterator = updatesToRemove.iterator(); while(iterator.hasNext()){ updates.remove(iterator.next()); } } /** * Inspects the current queue and marks eligible inserts, updates, and deletes * for coalesce. For example, if an object instance is marked for deletion and insertion, * this method will track the instance so that later the ActionQueue can remove both * the insert and delete from the queue. This results in no DML operation being executed * when true coalesce is enabled. This method also handles the case when an object that is queued * for INSERT, UPDATE, and DELETE is marked for coalesce. Update and Insert combinations that * are not queued for delete are coalesced by a call to tryToCoalesceUpdateIntoInsert from * the DefaultFlushEntityEventListener. * * @see #tryToCoalesceUpdateIntoInsert(Object, Object[]) * @see #coalesceExtendedPersistenceQueue() * */ public void prepareToCoalesceExtendedPersistenceQueue() { //if coalesce is disabled, don't do anything if(!useTrueCoalesce){ return; } //convert all insertions into a map where the key is object to be inserted //and the value is the EntityAction HashMap insertionsMap = new HashMap(insertions.size()); Iterator iterator = insertions.iterator(); while(iterator.hasNext()){ EntityAction insertAction = (EntityAction)iterator.next(); //only track inserts that are not inserts of identify //ideally we never should have an identity insert at flush time //but this is a sanity check if(!(insertAction instanceof EntityIdentityInsertAction)){ insertionsMap.put(insertAction.getInstance(), insertAction); } } //convert all updates into a map where the key is object to be inserted //and the value is the EntityAction HashMap updatesMap = new HashMap(updates.size()); iterator = updates.iterator(); while(iterator.hasNext()){ EntityAction updateAction = (EntityAction)iterator.next(); updatesMap.put(updateAction.getInstance(), updateAction); } //intialize the lists that will be used to track actions to coalesce insertionsToRemove = new ArrayList(); deletionsToRemove = new ArrayList(); updatesToRemove = new ArrayList(); /* * @TODO: Make sure that nothing is considered dirty after we "massage" the queue */ boolean removeDelete = false; //iterate all objects marked for deletion to determine if we need to coalesce any inserts and updates iterator = deletions.iterator(); while(iterator.hasNext()){ EntityAction deleteAction = (EntityAction)iterator.next(); //get the current instance marked for deletion from the queued action //we'll use this to find the object to coalesce from the insertions and updates maps final Object instanceMarkedForDeletion = deleteAction.getInstance(); //find any objects queued for insertion that are also //marked for deletion. If we find such an element, queue it up // for coalesce later to avoid ConcurrentModificationExceptions. if( insertionsMap.containsKey(instanceMarkedForDeletion)) { insertionsToRemove.add(insertionsMap.get(instanceMarkedForDeletion)); removeDelete = true; } //find any objects queued for update that are also //marked for deletion. If we find such an element, queue it up // for coalesce later to avoid ConcurrentModificationExceptions. if( updatesMap.containsKey(instanceMarkedForDeletion)) { updatesToRemove.add(updatesMap.get(instanceMarkedForDeletion)); //don't set the flag removeDelete that tells us later to remove the delete statement. //If the instance being updated is also being inserted, we already //have set the flag to remove the delete action. This is necessary //since an object being UPDATEd and not INSERTED will not be properly //DELETEd if we blindly indicate that the DELETE action be removed. //IOW, we should only prevent the DELETE statement if this //is a new object and not only an UPDATE. } //if we determined that the delete should be removed, queue it up // for coalesce later to avoid ConcurrentModificationExceptions. if(removeDelete){ //reset the flag removeDelete = false; deletionsToRemove.add(deleteAction); } } } /** * Retrieves all Entity instances queued for insertion * @return An array of mapped objects queue for insertion */ final public List getObjectsQueuedForInsertion(){ return retrieveQueuedObjectsFromList(insertions); } /** * Retrieves all Entity instances queued for update * @return An array of mapped objects queue for update */ final public List getObjectsQueuedForUpdate(){ return retrieveQueuedObjectsFromList(updates); } /** * Retrieves all Entity instances queued for deletion * @return An array of mapped objects queue for deletion */ final public List getObjectsQueuedForDeletion(){ return retrieveQueuedObjectsFromList(deletions); } /** * Retrieves all instances in the supplied queue * and returns them. If not entities exist, an empty * list will be returned. * @param queueEntityActions * @return */ private List retrieveQueuedObjectsFromList(final List queueEntityActions){ //if there are no objects in the queue, return null if(queueEntityActions.size()<1){ return java.util.Collections.emptyList(); } final List objects = new ArrayList(queueEntityActions.size()); final ListIterator iterator = queueEntityActions.listIterator(); while(iterator.hasNext()){ EntityAction entityAction = (EntityAction)iterator.next(); objects.add(entityAction.getInstance()); } return objects; } // end McKesson customizations public void addAction(EntityInsertAction action) { insertions.add( action ); } public void addAction(EntityDeleteAction action) { deletions.add( action ); } public void addAction(EntityUpdateAction action) { updates.add( action ); } public void addAction(CollectionRecreateAction action) { collectionCreations.add( action ); } public void addAction(CollectionRemoveAction action) { collectionRemovals.add( action ); } public void addAction(CollectionUpdateAction action) { collectionUpdates.add( action ); } public void addAction(EntityIdentityInsertAction insert) { insertions.add( insert ); } public void addAction(BulkOperationCleanupAction cleanupAction) { registerCleanupActions( cleanupAction ); } public void registerProcess(AfterTransactionCompletionProcess process) { afterTransactionProcesses.register( process ); } public void registerProcess(BeforeTransactionCompletionProcess process) { beforeTransactionProcesses.register( process ); } /** * Perform all currently queued entity-insertion actions. * * @throws HibernateException error executing queued insertion actions. */ public void executeInserts() throws HibernateException { executeActions( insertions ); } /** * Perform all currently queued actions. * * @throws HibernateException error executing queued actions. */ public void executeActions() throws HibernateException { if(useTrueCoalesce){ prepareToCoalesceExtendedPersistenceQueue(); coalesceExtendedPersistenceQueue(); } executeActions( insertions ); executeActions( updates ); executeActions( collectionRemovals ); executeActions( collectionUpdates ); executeActions( collectionCreations ); executeActions( deletions ); } /** * Prepares the internal action queues for execution. * * @throws HibernateException error preparing actions. */ public void prepareActions() throws HibernateException { prepareActions( collectionRemovals ); prepareActions( collectionUpdates ); prepareActions( collectionCreations ); } /** * Performs cleanup of any held cache softlocks. * * @param success Was the transaction successful. */ public void afterTransactionCompletion(boolean success) { afterTransactionProcesses.afterTransactionCompletion( success ); } /** * Execute any registered {@link BeforeTransactionCompletionProcess} */ public void beforeTransactionCompletion() { beforeTransactionProcesses.beforeTransactionCompletion(); } /** * Check whether the given tables/query-spaces are to be executed against * given the currently queued actions. * * @param tables The table/query-spaces to check. * * @return True if we contain pending actions against any of the given * tables; false otherwise. */ public boolean areTablesToBeUpdated(Set tables) { return areTablesToUpdated( updates, tables ) || areTablesToUpdated( insertions, tables ) || areTablesToUpdated( deletions, tables ) || areTablesToUpdated( collectionUpdates, tables ) || areTablesToUpdated( collectionCreations, tables ) || areTablesToUpdated( collectionRemovals, tables ); } /** * Check whether any insertion or deletion actions are currently queued. * * @return True if insertions or deletions are currently queued; false otherwise. */ public boolean areInsertionsOrDeletionsQueued() { return ( insertions.size() > 0 || deletions.size() > 0 ); } private static boolean areTablesToUpdated(List executables, Set tablespaces) { int size = executables.size(); for ( int j = 0; j < size; j++ ) { Serializable[] spaces = ( ( Executable ) executables.get( j ) ).getPropertySpaces(); for ( int i = 0; i < spaces.length; i++ ) { if ( tablespaces.contains( spaces[i] ) ) { if ( log.isDebugEnabled() ) { log.debug( "changes must be flushed to space: " + spaces[i] ); } return true; } } } return false; } private void executeActions(List list) throws HibernateException { int size = list.size(); for ( int i = 0; i < size; i++ ) { execute( ( Executable ) list.get( i ) ); } list.clear(); session.getBatcher().executeBatch(); } public void execute(Executable executable) { try { executable.execute(); } finally { registerCleanupActions( executable ); } } private void registerCleanupActions(Executable executable) { beforeTransactionProcesses.register( executable.getBeforeTransactionCompletionProcess() ); if ( session.getFactory().getSettings().isQueryCacheEnabled() ) { final String[] spaces = (String[]) executable.getPropertySpaces(); afterTransactionProcesses.addSpacesToInvalidate( spaces ); session.getFactory().getUpdateTimestampsCache().preinvalidate( spaces ); } afterTransactionProcesses.register( executable.getAfterTransactionCompletionProcess() ); } private void prepareActions(List queue) throws HibernateException { int size = queue.size(); for ( int i = 0; i < size; i++ ) { Executable executable = ( Executable ) queue.get( i ); executable.beforeExecutions(); } } /** * Returns a string representation of the object. * * @return a string representation of the object. */ public String toString() { return new StringBuffer() .append( "ActionQueue[insertions=" ).append( insertions ) .append( " updates=" ).append( updates ) .append( " deletions=" ).append( deletions ) .append( " collectionCreations=" ).append( collectionCreations ) .append( " collectionRemovals=" ).append( collectionRemovals ) .append( " collectionUpdates=" ).append( collectionUpdates ) .append( "]" ) .toString(); } public int numberOfCollectionRemovals() { return collectionRemovals.size(); } public int numberOfCollectionUpdates() { return collectionUpdates.size(); } public int numberOfCollectionCreations() { return collectionCreations.size(); } public int numberOfDeletions() { return deletions.size(); } public int numberOfUpdates() { return updates.size(); } public int numberOfInsertions() { return insertions.size(); } public void sortCollectionActions() { if ( session.getFactory().getSettings().isOrderUpdatesEnabled() ) { //sort the updates by fk java.util.Collections.sort( collectionCreations ); java.util.Collections.sort( collectionUpdates ); java.util.Collections.sort( collectionRemovals ); } } public void sortActions() { if ( session.getFactory().getSettings().isOrderUpdatesEnabled() ) { //sort the updates by pk java.util.Collections.sort( updates ); } if ( session.getFactory().getSettings().isOrderInsertsEnabled() ) { sortInsertActions(); } } /** * Order the {@link #insertions} queue such that we group inserts * against the same entity together (without violating constraints). The * original order is generated by cascade order, which in turn is based on * the directionality of foreign-keys. So even though we will be changing * the ordering here, we need to make absolutely certain that we do not * circumvent this FK ordering to the extent of causing constraint * violations */ private void sortInsertActions() { new InsertActionSorter().sort(); } public ArrayList cloneDeletions() { return ( ArrayList ) deletions.clone(); } public void clearFromFlushNeededCheck(int previousCollectionRemovalSize) { collectionCreations.clear(); collectionUpdates.clear(); updates.clear(); // collection deletions are a special case since update() can add // deletions of collections not loaded by the session. for ( int i = collectionRemovals.size() - 1; i >= previousCollectionRemovalSize; i-- ) { collectionRemovals.remove( i ); } } public boolean hasAfterTransactionActions() { return afterTransactionProcesses.processes.size() > 0; } public boolean hasBeforeTransactionActions() { return beforeTransactionProcesses.processes.size() > 0; } public boolean hasAnyQueuedActions() { return updates.size() > 0 || insertions.size() > 0 || deletions.size() > 0 || collectionUpdates.size() > 0 || collectionRemovals.size() > 0 || collectionCreations.size() > 0; } /** * Used by the owning session to explicitly control serialization of the * action queue * * @param oos The stream to which the action queue should get written * * @throws IOException Indicates an error writing to the stream */ public void serialize(ObjectOutputStream oos) throws IOException { log.trace( "serializing action-queue" ); int queueSize = insertions.size(); log.trace( "starting serialization of [" + queueSize + "] insertions entries" ); oos.writeInt( queueSize ); for ( int i = 0; i < queueSize; i++ ) { oos.writeObject( insertions.get( i ) ); } queueSize = deletions.size(); log.trace( "starting serialization of [" + queueSize + "] deletions entries" ); oos.writeInt( queueSize ); for ( int i = 0; i < queueSize; i++ ) { oos.writeObject( deletions.get( i ) ); } queueSize = updates.size(); log.trace( "starting serialization of [" + queueSize + "] updates entries" ); oos.writeInt( queueSize ); for ( int i = 0; i < queueSize; i++ ) { oos.writeObject( updates.get( i ) ); } queueSize = collectionUpdates.size(); log.trace( "starting serialization of [" + queueSize + "] collectionUpdates entries" ); oos.writeInt( queueSize ); for ( int i = 0; i < queueSize; i++ ) { oos.writeObject( collectionUpdates.get( i ) ); } queueSize = collectionRemovals.size(); log.trace( "starting serialization of [" + queueSize + "] collectionRemovals entries" ); oos.writeInt( queueSize ); for ( int i = 0; i < queueSize; i++ ) { oos.writeObject( collectionRemovals.get( i ) ); } queueSize = collectionCreations.size(); log.trace( "starting serialization of [" + queueSize + "] collectionCreations entries" ); oos.writeInt( queueSize ); for ( int i = 0; i < queueSize; i++ ) { oos.writeObject( collectionCreations.get( i ) ); } } /** * Used by the owning session to explicitly control deserialization of the * action queue * * @param ois The stream from which to read the action queue * @param session The session to which the action queue belongs * * @return The deserialized action queue * * @throws IOException indicates a problem reading from the stream * @throws ClassNotFoundException Generally means we were unable to locate user classes. */ public static ActionQueue deserialize( ObjectInputStream ois, SessionImplementor session) throws IOException, ClassNotFoundException { log.trace( "deserializing action-queue" ); ActionQueue rtn = new ActionQueue( session ); int queueSize = ois.readInt(); log.trace( "starting deserialization of [" + queueSize + "] insertions entries" ); rtn.insertions = new ArrayList( queueSize ); for ( int i = 0; i < queueSize; i++ ) { rtn.insertions.add( ois.readObject() ); } queueSize = ois.readInt(); log.trace( "starting deserialization of [" + queueSize + "] deletions entries" ); rtn.deletions = new ArrayList( queueSize ); for ( int i = 0; i < queueSize; i++ ) { rtn.deletions.add( ois.readObject() ); } queueSize = ois.readInt(); log.trace( "starting deserialization of [" + queueSize + "] updates entries" ); rtn.updates = new ArrayList( queueSize ); for ( int i = 0; i < queueSize; i++ ) { rtn.updates.add( ois.readObject() ); } queueSize = ois.readInt(); log.trace( "starting deserialization of [" + queueSize + "] collectionUpdates entries" ); rtn.collectionUpdates = new ArrayList( queueSize ); for ( int i = 0; i < queueSize; i++ ) { rtn.collectionUpdates.add( ois.readObject() ); } queueSize = ois.readInt(); log.trace( "starting deserialization of [" + queueSize + "] collectionRemovals entries" ); rtn.collectionRemovals = new ArrayList( queueSize ); for ( int i = 0; i < queueSize; i++ ) { rtn.collectionRemovals.add( ois.readObject() ); } queueSize = ois.readInt(); log.trace( "starting deserialization of [" + queueSize + "] collectionCreations entries" ); rtn.collectionCreations = new ArrayList( queueSize ); for ( int i = 0; i < queueSize; i++ ) { rtn.collectionCreations.add( ois.readObject() ); } return rtn; } private static class BeforeTransactionCompletionProcessQueue { private SessionImplementor session; private List processes = new ArrayList(); private BeforeTransactionCompletionProcessQueue(SessionImplementor session) { this.session = session; } public void register(BeforeTransactionCompletionProcess process) { if ( process == null ) { return; } processes.add( process ); } public void beforeTransactionCompletion() { final int size = processes.size(); for ( int i = 0; i < size; i++ ) { try { BeforeTransactionCompletionProcess process = ( BeforeTransactionCompletionProcess ) processes.get( i ); process.doBeforeTransactionCompletion( session ); } catch ( HibernateException he ) { throw he; } catch ( Exception e ) { throw new AssertionFailure( "Unable to perform beforeTransactionCompletion callback", e ); } } processes.clear(); } } private static class AfterTransactionCompletionProcessQueue { private SessionImplementor session; private Set querySpacesToInvalidate = new HashSet(); private List processes = new ArrayList( INIT_QUEUE_LIST_SIZE * 3 ); private AfterTransactionCompletionProcessQueue(SessionImplementor session) { this.session = session; } public void addSpacesToInvalidate(String[] spaces) { if ( spaces == null ) { return; } for ( int i = 0, max = spaces.length; i < max; i++ ) { addSpaceToInvalidate( spaces[i] ); } } public void addSpaceToInvalidate(String space) { querySpacesToInvalidate.add( space ); } public void register(AfterTransactionCompletionProcess process) { if ( process == null ) { return; } processes.add( process ); } public void afterTransactionCompletion(boolean success) { final int size = processes.size(); for ( int i = 0; i < size; i++ ) { try { AfterTransactionCompletionProcess process = ( AfterTransactionCompletionProcess ) processes.get( i ); process.doAfterTransactionCompletion( success, session ); } catch ( CacheException ce ) { log.error( "could not release a cache lock", ce ); // continue loop } catch ( Exception e ) { throw new AssertionFailure( "Exception releasing cache locks", e ); } } processes.clear(); if ( session.getFactory().getSettings().isQueryCacheEnabled() ) { session.getFactory().getUpdateTimestampsCache().invalidate( ( String[] ) querySpacesToInvalidate.toArray( new String[ querySpacesToInvalidate.size()] ) ); } querySpacesToInvalidate.clear(); } } /** * Sorts the insert actions using more hashes. * * @author Jay Erb */ private class InsertActionSorter { // the mapping of entity names to their latest batch numbers. private HashMap latestBatches = new HashMap(); private HashMap entityBatchNumber; // the map of batch numbers to EntityInsertAction lists private HashMap actionBatches = new HashMap(); public InsertActionSorter() { //optimize the hash size to eliminate a rehash. entityBatchNumber = new HashMap( insertions.size() + 1, 1.0f ); } /** * Sort the insert actions. */ public void sort() { // the list of entity names that indicate the batch number for ( Iterator actionItr = insertions.iterator(); actionItr.hasNext(); ) { EntityInsertAction action = ( EntityInsertAction ) actionItr.next(); // remove the current element from insertions. It will be added back later. String entityName = action.getEntityName(); // the entity associated with the current action. Object currentEntity = action.getInstance(); Integer batchNumber; if ( latestBatches.containsKey( entityName ) ) { // There is already an existing batch for this type of entity. // Check to see if the latest batch is acceptable. batchNumber = findBatchNumber( action, entityName ); } else { // add an entry for this type of entity. // we can be assured that all referenced entities have already // been processed, // so specify that this entity is with the latest batch. // doing the batch number before adding the name to the list is // a faster way to get an accurate number. batchNumber = new Integer( actionBatches.size() ); latestBatches.put( entityName, batchNumber ); } entityBatchNumber.put( currentEntity, batchNumber ); addToBatch( batchNumber, action ); } insertions.clear(); // now rebuild the insertions list. There is a batch for each entry in the name list. for ( int i = 0; i < actionBatches.size(); i++ ) { List batch = ( List ) actionBatches.get( new Integer( i ) ); for ( Iterator batchItr = batch.iterator(); batchItr.hasNext(); ) { EntityInsertAction action = ( EntityInsertAction ) batchItr.next(); insertions.add( action ); } } } /** * Finds an acceptable batch for this entity to be a member as part of the {@link InsertActionSorter} * * @param action The action being sorted * @param entityName The name of the entity affected by the action * * @return An appropriate batch number; todo document this process better */ private Integer findBatchNumber( EntityInsertAction action, String entityName) { // loop through all the associated entities and make sure they have been // processed before the latest // batch associated with this entity type. // the current batch number is the latest batch for this entity type. Integer latestBatchNumberForType = ( Integer ) latestBatches.get( entityName ); // loop through all the associations of the current entity and make sure that they are processed // before the current batch number Object[] propertyValues = action.getState(); Type[] propertyTypes = action.getPersister().getClassMetadata() .getPropertyTypes(); for ( int i = 0; i < propertyValues.length; i++ ) { Object value = propertyValues[i]; Type type = propertyTypes[i]; if ( type.isEntityType() && value != null ) { // find the batch number associated with the current association, if any. Integer associationBatchNumber = ( Integer ) entityBatchNumber.get( value ); if ( associationBatchNumber != null && associationBatchNumber.compareTo( latestBatchNumberForType ) > 0 ) { // create a new batch for this type. The batch number is the number of current batches. latestBatchNumberForType = new Integer( actionBatches.size() ); latestBatches.put( entityName, latestBatchNumberForType ); // since this entity will now be processed in the latest possible batch, // we can be assured that it will come after all other associations, // there's not need to continue checking. break; } } } return latestBatchNumberForType; } private void addToBatch(Integer batchNumber, EntityInsertAction action) { List actions = ( List ) actionBatches.get( batchNumber ); if ( actions == null ) { actions = new LinkedList(); actionBatches.put( batchNumber, actions ); } actions.add( action ); } } }