View Javadoc
1   /*
2    * Hibernate, Relational Persistence for Idiomatic Java
3    *
4    * Copyright (c) 2008, Red Hat Middleware LLC or third-party contributors as
5    * indicated by the @author tags or express copyright attribution
6    * statements applied by the authors.  All third-party contributions are
7    * distributed under license by Red Hat Middleware LLC.
8    *
9    * This copyrighted material is made available to anyone wishing to use, modify,
10   * copy, or redistribute it subject to the terms and conditions of the GNU
11   * Lesser General Public License, as published by the Free Software Foundation.
12   *
13   * This program is distributed in the hope that it will be useful,
14   * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
15   * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
16   * for more details.
17   *
18   * You should have received a copy of the GNU Lesser General Public License
19   * along with this distribution; if not, write to:
20   * Free Software Foundation, Inc.
21   * 51 Franklin Street, Fifth Floor
22   * Boston, MA  02110-1301  USA
23   *
24   */
25  package org.hibernate.engine.loading;
26  
27  import org.hibernate.CacheMode;
28  import org.hibernate.EntityMode;
29  import org.hibernate.HibernateException;
30  import org.hibernate.cache.CacheKey;
31  import org.hibernate.cache.entry.CollectionCacheEntry;
32  import org.hibernate.collection.PersistentCollection;
33  import org.hibernate.engine.*;
34  import org.hibernate.persister.collection.CollectionPersister;
35  import org.hibernate.pretty.MessageHelper;
36  import org.slf4j.Logger;
37  import org.slf4j.LoggerFactory;
38  
39  import java.io.Serializable;
40  import java.sql.ResultSet;
41  import java.util.*;
42  
43  /**
44   * Represents state associated with the processing of a given {@link ResultSet}
45   * in regards to loading collections.
46   * <p/>
47   * Another implementation option to consider is to not expose {@link ResultSet}s
48   * directly (in the JDBC redesign) but to always "wrap" them and apply a
49   * [series of] context[s] to that wrapper.
50   *
51   * @author Steve Ebersole
52   */
53  public class CollectionLoadContext {
54  	private static final Logger log = LoggerFactory.getLogger( CollectionLoadContext.class );
55  
56  	private final LoadContexts loadContexts;
57  	private final ResultSet resultSet;
58  	private Set localLoadingCollectionKeys = new HashSet();
59  
60  	/**
61  	 * Creates a collection load context for the given result set.
62  	 *
63  	 * @param loadContexts Callback to other collection load contexts.
64  	 * @param resultSet The result set this is "wrapping".
65  	 */
66  	public CollectionLoadContext(LoadContexts loadContexts, ResultSet resultSet) {
67  		this.loadContexts = loadContexts;
68  		this.resultSet = resultSet;
69  	}
70  
71  	public ResultSet getResultSet() {
72  		return resultSet;
73  	}
74  
75  	public LoadContexts getLoadContext() {
76  		return loadContexts;
77  	}
78  
79  	/**
80  	 * Retrieve the collection that is being loaded as part of processing this
81  	 * result set.
82  	 * <p/>
83  	 * Basically, there are two valid return values from this method:<ul>
84  	 * <li>an instance of {@link PersistentCollection} which indicates to
85  	 * continue loading the result set row data into that returned collection
86  	 * instance; this may be either an instance already associated and in the
87  	 * midst of being loaded, or a newly instantiated instance as a matching
88  	 * associated collection was not found.</li>
89  	 * <li><i>null</i> indicates to ignore the corresponding result set row
90  	 * data relating to the requested collection; this indicates that either
91  	 * the collection was found to already be associated with the persistence
92  	 * context in a fully loaded state, or it was found in a loading state
93  	 * associated with another result set processing context.</li>
94  	 * </ul>
95  	 *
96  	 * @param persister The persister for the collection being requested.
97  	 * @param key The key of the collection being requested.
98  	 *
99  	 * @return The loading collection (see discussion above).
100 	 */
101 	public PersistentCollection getLoadingCollection(final CollectionPersister persister, final Serializable key) {
102 		final EntityMode em = loadContexts.getPersistenceContext().getSession().getEntityMode();
103 		final CollectionKey collectionKey = new CollectionKey( persister, key, em );
104 		if ( log.isTraceEnabled() ) {
105 			log.trace( "starting attempt to find loading collection [" + MessageHelper.collectionInfoString( persister.getRole(), key ) + "]" );
106 		}
107 		final LoadingCollectionEntry loadingCollectionEntry = loadContexts.locateLoadingCollectionEntry( collectionKey );
108 		if ( loadingCollectionEntry == null ) {
109 			// look for existing collection as part of the persistence context
110 			PersistentCollection collection = loadContexts.getPersistenceContext().getCollection( collectionKey );
111 			if ( collection != null ) {
112 				if ( collection.wasInitialized() ) {
113 					log.trace( "collection already initialized; ignoring" );
114 					return null; // ignore this row of results! Note the early exit
115 				}
116 				else {
117 					// initialize this collection
118 					log.trace( "collection not yet initialized; initializing" );
119 				}
120 			}
121 			else {
122 				Object owner = loadContexts.getPersistenceContext().getCollectionOwner( key, persister );
123 				final boolean newlySavedEntity = owner != null
124 						&& loadContexts.getPersistenceContext().getEntry( owner ).getStatus() != Status.LOADING
125 						&& em != EntityMode.DOM4J;
126 				if ( newlySavedEntity ) {
127 					// important, to account for newly saved entities in query
128 					// todo : some kind of check for new status...
129 					log.trace( "owning entity already loaded; ignoring" );
130 					return null;
131 				}
132 				else {
133 					// create one
134 					if ( log.isTraceEnabled() ) {
135 						log.trace( "instantiating new collection [key=" + key + ", rs=" + resultSet + "]" );
136 					}
137 					collection = persister.getCollectionType()
138 							.instantiate( loadContexts.getPersistenceContext().getSession(), persister, key );
139 				}
140 			}
141 			collection.beforeInitialize( persister, -1 );
142 			collection.beginRead();
143 			localLoadingCollectionKeys.add( collectionKey );
144 			loadContexts.registerLoadingCollectionXRef( collectionKey, new LoadingCollectionEntry( resultSet, persister, key, collection ) );
145 			return collection;
146 		}
147 		else {
148 			if ( loadingCollectionEntry.getResultSet() == resultSet ) {
149 				log.trace( "found loading collection bound to current result set processing; reading row" );
150 				return loadingCollectionEntry.getCollection();
151 			}
152 			else {
153 				// ignore this row, the collection is in process of
154 				// being loaded somewhere further "up" the stack
155 				log.trace( "collection is already being initialized; ignoring row" );
156 				return null;
157 			}
158 		}
159 	}
160 
161 	/**
162 	 * Finish the process of collection-loading for this bound result set.  Mainly this
163 	 * involves cleaning up resources and notifying the collections that loading is
164 	 * complete.
165 	 *
166 	 * @param persister The persister for which to complete loading.
167 	 */
168 	public void endLoadingCollections(CollectionPersister persister) {
169 		SessionImplementor session = getLoadContext().getPersistenceContext().getSession();
170 		if ( !loadContexts.hasLoadingCollectionEntries()
171 				&& localLoadingCollectionKeys.isEmpty() ) {
172 			return;
173 		}
174 
175 		// in an effort to avoid concurrent-modification-exceptions (from
176 		// potential recursive calls back through here as a result of the
177 		// eventual call to PersistentCollection#endRead), we scan the
178 		// internal loadingCollections map for matches and store those matches
179 		// in a temp collection.  the temp collection is then used to "drive"
180 		// the #endRead processing.
181 		List matches = null;
182 		Iterator iter = localLoadingCollectionKeys.iterator();
183 		while ( iter.hasNext() ) {
184 			final CollectionKey collectionKey = (CollectionKey) iter.next();
185 			final LoadingCollectionEntry lce = loadContexts.locateLoadingCollectionEntry( collectionKey );
186 			if ( lce == null) {
187 				log.warn( "In CollectionLoadContext#endLoadingCollections, localLoadingCollectionKeys contained [" + collectionKey + "], but no LoadingCollectionEntry was found in loadContexts" );
188 			}
189 			else if ( lce.getResultSet() == resultSet && lce.getPersister() == persister ) {
190 				if ( matches == null ) {
191 					matches = new ArrayList();
192 				}
193 				matches.add( lce );
194 				if ( lce.getCollection().getOwner() == null ) {
195 					session.getPersistenceContext().addUnownedCollection(
196 							new CollectionKey( persister, lce.getKey(), session.getEntityMode() ),
197 							lce.getCollection()
198 					);
199 				}
200 				if ( log.isTraceEnabled() ) {
201 					log.trace( "removing collection load entry [" + lce + "]" );
202 				}
203 
204 				// todo : i'd much rather have this done from #endLoadingCollection(CollectionPersister,LoadingCollectionEntry)...
205 				loadContexts.unregisterLoadingCollectionXRef( collectionKey );
206 				iter.remove();
207 			}
208 		}
209 
210 		endLoadingCollections( persister, matches );
211 		if ( localLoadingCollectionKeys.isEmpty() ) {
212 			// todo : hack!!!
213 			// NOTE : here we cleanup the load context when we have no more local
214 			// LCE entries.  This "works" for the time being because really
215 			// only the collection load contexts are implemented.  Long term,
216 			// this cleanup should become part of the "close result set"
217 			// processing from the (sandbox/jdbc) jdbc-container code.
218 			loadContexts.cleanup( resultSet );
219 		}
220 	}
221 
222 	private void endLoadingCollections(CollectionPersister persister, List matchedCollectionEntries) {
223 		if ( matchedCollectionEntries == null ) {
224 			if ( log.isDebugEnabled() ) {
225 				log.debug( "no collections were found in result set for role: " + persister.getRole() );
226 			}
227 			return;
228 		}
229 
230 		final int count = matchedCollectionEntries.size();
231 		if ( log.isDebugEnabled() ) {
232 			log.debug( count + " collections were found in result set for role: " + persister.getRole() );
233 		}
234 
235 		for ( int i = 0; i < count; i++ ) {
236 			LoadingCollectionEntry lce = ( LoadingCollectionEntry ) matchedCollectionEntries.get( i );
237 			endLoadingCollection( lce, persister );
238 		}
239 
240 		if ( log.isDebugEnabled() ) {
241 			log.debug( count + " collections initialized for role: " + persister.getRole() );
242 		}
243 	}
244 
245 	private void endLoadingCollection(LoadingCollectionEntry lce, CollectionPersister persister) {
246 		if ( log.isTraceEnabled() ) {
247 			log.debug( "ending loading collection [" + lce + "]" );
248 		}
249 		final SessionImplementor session = getLoadContext().getPersistenceContext().getSession();
250 		final EntityMode em = session.getEntityMode();
251 
252 		boolean hasNoQueuedAdds = lce.getCollection().endRead(); // warning: can cause a recursive calls! (proxy initialization)
253 
254 		if ( persister.getCollectionType().hasHolder( em ) ) {
255 			getLoadContext().getPersistenceContext().addCollectionHolder( lce.getCollection() );
256 		}
257 
258 		CollectionEntry ce = getLoadContext().getPersistenceContext().getCollectionEntry( lce.getCollection() );
259 		if ( ce == null ) {
260 			ce = getLoadContext().getPersistenceContext().addInitializedCollection( persister, lce.getCollection(), lce.getKey() );
261 		}
262 		else {
263 			ce.postInitialize( lce.getCollection() );
264 		}
265 
266 		boolean addToCache = hasNoQueuedAdds && // there were no queued additions
267 				persister.hasCache() &&             // and the role has a cache
268 				session.getCacheMode().isPutEnabled() &&
269 				!ce.isDoremove();                   // and this is not a forced initialization during flush
270 		if ( addToCache ) {
271 			addCollectionToCache( lce, persister );
272 		}
273 
274 		if ( log.isDebugEnabled() ) {
275 			log.debug( "collection fully initialized: " + MessageHelper.collectionInfoString(persister, lce.getKey(), session.getFactory() ) );
276 		}
277 
278 		if ( session.getFactory().getStatistics().isStatisticsEnabled() ) {
279 			session.getFactory().getStatisticsImplementor().loadCollection( persister.getRole() );
280 		}
281 	}
282 
283 	/**
284 	 * Add the collection to the second-level cache
285 	 *
286 	 * @param lce The entry representing the collection to add
287 	 * @param persister The persister
288 	 */
289 	private void addCollectionToCache(LoadingCollectionEntry lce, CollectionPersister persister) {
290 		final SessionImplementor session = getLoadContext().getPersistenceContext().getSession();
291 		final SessionFactoryImplementor factory = session.getFactory();
292 
293 		if ( log.isDebugEnabled() ) {
294 			log.debug( "Caching collection: " + MessageHelper.collectionInfoString( persister, lce.getKey(), factory ) );
295 		}
296 
297 		if ( !session.getEnabledFilters().isEmpty() && persister.isAffectedByEnabledFilters( session ) ) {
298 			// some filters affecting the collection are enabled on the session, so do not do the put into the cache.
299 			log.debug( "Refusing to add to cache due to enabled filters" );
300 			// todo : add the notion of enabled filters to the CacheKey to differentiate filtered collections from non-filtered;
301 			//      but CacheKey is currently used for both collections and entities; would ideally need to define two seperate ones;
302 			//      currently this works in conjuction with the check on
303 			//      DefaultInitializeCollectionEventHandler.initializeCollectionFromCache() (which makes sure to not read from
304 			//      cache with enabled filters).
305 			return; // EARLY EXIT!!!!!
306 		}
307 
308 		final Object version;
309 		if ( persister.isVersioned() ) {
310 			Object collectionOwner = getLoadContext().getPersistenceContext().getCollectionOwner( lce.getKey(), persister );
311 			if ( collectionOwner == null ) {
312 				// generally speaking this would be caused by the collection key being defined by a property-ref, thus
313 				// the collection key and the owner key would not match up.  In this case, try to use the key of the
314 				// owner instance associated with the collection itself, if one.  If the collection does already know
315 				// about its owner, that owner should be the same instance as associated with the PC, but we do the
316 				// resolution against the PC anyway just to be safe since the lookup should not be costly.
317 				if ( lce.getCollection() != null ) {
318 					Object linkedOwner = lce.getCollection().getOwner();
319 					if ( linkedOwner != null ) {
320 						final Serializable ownerKey = persister.getOwnerEntityPersister().getIdentifier( linkedOwner, session );
321 						collectionOwner = getLoadContext().getPersistenceContext().getCollectionOwner( ownerKey, persister );
322 					}
323 				}
324 				if ( collectionOwner == null ) {
325 					throw new HibernateException(
326 							"Unable to resolve owner of loading collection [" +
327 									MessageHelper.collectionInfoString( persister, lce.getKey(), factory ) +
328 									"] for second level caching"
329 					);
330 				}
331 			}
332 			version = getLoadContext().getPersistenceContext().getEntry( collectionOwner ).getVersion();
333 		}
334 		else {
335 			version = null;
336 		}
337 
338 		CollectionCacheEntry entry = new CollectionCacheEntry( lce.getCollection(), persister );
339 		CacheKey cacheKey = new CacheKey(
340 				lce.getKey(),
341 				persister.getKeyType(),
342 				persister.getRole(),
343 				session.getEntityMode(),
344 				session.getFactory()
345 		);
346 		boolean put = persister.getCacheAccessStrategy().putFromLoad(
347 				cacheKey,
348 				persister.getCacheEntryStructure().structure(entry),
349 				session.getTimestamp(),
350 				version,
351 				factory.getSettings().isMinimalPutsEnabled() && session.getCacheMode()!= CacheMode.REFRESH
352 		);
353 
354 		if ( put && factory.getStatistics().isStatisticsEnabled() ) {
355 			factory.getStatisticsImplementor().secondLevelCachePut( persister.getCacheAccessStrategy().getRegion().getName() );
356 		}
357 	}
358 
359 	void cleanup() {
360 		if ( !localLoadingCollectionKeys.isEmpty() ) {
361 			log.warn( "On CollectionLoadContext#cleanup, localLoadingCollectionKeys contained [" + localLoadingCollectionKeys.size() + "] entries" );
362 		}
363 		loadContexts.cleanupCollectionXRefs( localLoadingCollectionKeys );
364 		localLoadingCollectionKeys.clear();
365 	}
366 
367 
368 	public String toString() {
369 		return super.toString() + "<rs=" + resultSet + ">";
370 	}
371 }