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.event.def;
26
27 import java.io.Serializable;
28
29 import org.slf4j.Logger;
30 import org.slf4j.LoggerFactory;
31 import org.hibernate.AssertionFailure;
32 import org.hibernate.EntityMode;
33 import org.hibernate.HibernateException;
34 import org.hibernate.StaleObjectStateException;
35 import org.hibernate.action.EntityUpdateAction;
36 import org.hibernate.action.DelayedPostInsertIdentifier;
37 import org.hibernate.classic.Validatable;
38 import org.hibernate.engine.EntityEntry;
39 import org.hibernate.engine.EntityKey;
40 import org.hibernate.engine.Nullability;
41 import org.hibernate.engine.SessionImplementor;
42 import org.hibernate.engine.Status;
43 import org.hibernate.engine.Versioning;
44 import org.hibernate.event.EventSource;
45 import org.hibernate.event.FlushEntityEvent;
46 import org.hibernate.event.FlushEntityEventListener;
47 import org.hibernate.intercept.FieldInterceptionHelper;
48 import org.hibernate.persister.entity.EntityPersister;
49 import org.hibernate.pretty.MessageHelper;
50 import org.hibernate.type.Type;
51 import org.hibernate.util.ArrayHelper;
52
53 /**
54 * An event that occurs for each entity instance at flush time
55 *
56 * @author Gavin King
57 */
58 public class DefaultFlushEntityEventListener implements FlushEntityEventListener {
59
60 private static final Logger log = LoggerFactory.getLogger(DefaultFlushEntityEventListener.class);
61
62 /**
63 * make sure user didn't mangle the id
64 */
65 public void checkId(Object object, EntityPersister persister, Serializable id, EntityMode entityMode)
66 throws HibernateException {
67
68 if ( id != null && id instanceof DelayedPostInsertIdentifier ) {
69 // this is a situation where the entity id is assigned by a post-insert generator
70 // and was saved outside the transaction forcing it to be delayed
71 return;
72 }
73
74 if ( persister.canExtractIdOutOfEntity() ) {
75
76 Serializable oid = persister.getIdentifier( object, entityMode );
77 if (id==null) {
78 throw new AssertionFailure("null id in " + persister.getEntityName() + " entry (don't flush the Session after an exception occurs)");
79 }
80 if ( !persister.getIdentifierType().isEqual(id, oid, entityMode) ) {
81 throw new HibernateException(
82 "identifier of an instance of " +
83 persister.getEntityName() +
84 " was altered from " + id +
85 " to " + oid
86 );
87 }
88 }
89
90 }
91
92 private void checkNaturalId(
93 EntityPersister persister,
94 EntityEntry entry,
95 Object[] current,
96 Object[] loaded,
97 EntityMode entityMode,
98 SessionImplementor session) {
99 if ( persister.hasNaturalIdentifier() && entry.getStatus() != Status.READ_ONLY ) {
100 Object[] snapshot = null;
101 Type[] types = persister.getPropertyTypes();
102 int[] props = persister.getNaturalIdentifierProperties();
103 boolean[] updateable = persister.getPropertyUpdateability();
104 for ( int i=0; i<props.length; i++ ) {
105 int prop = props[i];
106 if ( !updateable[prop] ) {
107 Object loadedVal;
108 if ( loaded == null ) {
109 if ( snapshot == null) {
110 snapshot = session.getPersistenceContext().getNaturalIdSnapshot( entry.getId(), persister );
111 }
112 loadedVal = snapshot[i];
113 } else {
114 loadedVal = loaded[prop];
115 }
116 if ( !types[prop].isEqual( current[prop], loadedVal, entityMode ) ) {
117 throw new HibernateException(
118 "immutable natural identifier of an instance of " +
119 persister.getEntityName() +
120 " was altered"
121 );
122 }
123 }
124 }
125 }
126 }
127
128 /**
129 * Flushes a single entity's state to the database, by scheduling
130 * an update action, if necessary
131 */
132 public void onFlushEntity(FlushEntityEvent event) throws HibernateException {
133 final Object entity = event.getEntity();
134 final EntityEntry entry = event.getEntityEntry();
135 final EventSource session = event.getSession();
136 final EntityPersister persister = entry.getPersister();
137 final Status status = entry.getStatus();
138 final EntityMode entityMode = session.getEntityMode();
139 final Type[] types = persister.getPropertyTypes();
140
141 final boolean mightBeDirty = entry.requiresDirtyCheck(entity);
142
143 final Object[] values = getValues( entity, entry, entityMode, mightBeDirty, session );
144
145 event.setPropertyValues(values);
146
147 //TODO: avoid this for non-new instances where mightBeDirty==false
148 boolean substitute = wrapCollections( session, persister, types, values);
149
150 if ( isUpdateNecessary( event, mightBeDirty ) ) {
151 substitute = scheduleUpdate( event ) || substitute;
152 }
153
154 if ( status != Status.DELETED ) {
155 // now update the object .. has to be outside the main if block above (because of collections)
156 if (substitute) persister.setPropertyValues( entity, values, entityMode );
157
158 // Search for collections by reachability, updating their role.
159 // We don't want to touch collections reachable from a deleted object
160 if ( persister.hasCollections() ) {
161 new FlushVisitor(session, entity).processEntityPropertyValues(values, types);
162 }
163 }
164
165 }
166
167 private Object[] getValues(
168 Object entity,
169 EntityEntry entry,
170 EntityMode entityMode,
171 boolean mightBeDirty,
172 SessionImplementor session
173 ) {
174 final Object[] loadedState = entry.getLoadedState();
175 final Status status = entry.getStatus();
176 final EntityPersister persister = entry.getPersister();
177
178 final Object[] values;
179 if ( status == Status.DELETED ) {
180 //grab its state saved at deletion
181 values = entry.getDeletedState();
182 }
183 else if ( !mightBeDirty && loadedState!=null ) {
184 values = loadedState;
185 }
186 else {
187 checkId( entity, persister, entry.getId(), entityMode );
188
189 // grab its current state
190 values = persister.getPropertyValues( entity, entityMode );
191
192 checkNaturalId( persister, entry, values, loadedState, entityMode, session );
193 }
194 return values;
195 }
196
197 private boolean wrapCollections(
198 EventSource session,
199 EntityPersister persister,
200 Type[] types,
201 Object[] values
202 ) {
203 if ( persister.hasCollections() ) {
204
205 // wrap up any new collections directly referenced by the object
206 // or its components
207
208 // NOTE: we need to do the wrap here even if its not "dirty",
209 // because collections need wrapping but changes to _them_
210 // don't dirty the container. Also, for versioned data, we
211 // need to wrap before calling searchForDirtyCollections
212
213 WrapVisitor visitor = new WrapVisitor(session);
214 // substitutes into values by side-effect
215 visitor.processEntityPropertyValues(values, types);
216 return visitor.isSubstitutionRequired();
217 }
218 else {
219 return false;
220 }
221 }
222
223 private boolean isUpdateNecessary(final FlushEntityEvent event, final boolean mightBeDirty) {
224 final Status status = event.getEntityEntry().getStatus();
225 if ( mightBeDirty || status==Status.DELETED ) {
226 // compare to cached state (ignoring collections unless versioned)
227 dirtyCheck(event);
228 if ( isUpdateNecessary(event) ) {
229 return true;
230 }
231 else {
232 FieldInterceptionHelper.clearDirty( event.getEntity() );
233 return false;
234 }
235 }
236 else {
237 return hasDirtyCollections( event, event.getEntityEntry().getPersister(), status );
238 }
239 }
240
241 private boolean scheduleUpdate(final FlushEntityEvent event) {
242
243 final EntityEntry entry = event.getEntityEntry();
244 final EventSource session = event.getSession();
245 final Object entity = event.getEntity();
246 final Status status = entry.getStatus();
247 final EntityMode entityMode = session.getEntityMode();
248 final EntityPersister persister = entry.getPersister();
249 final Object[] values = event.getPropertyValues();
250
251 if ( log.isTraceEnabled() ) {
252 if ( status == Status.DELETED ) {
253 log.trace(
254 "Updating deleted entity: " +
255 MessageHelper.infoString( persister, entry.getId(), session.getFactory() )
256 );
257 }
258 else {
259 log.trace(
260 "Updating entity: " +
261 MessageHelper.infoString( persister, entry.getId(), session.getFactory() )
262 );
263 }
264 }
265
266 final boolean intercepted;
267 if ( !entry.isBeingReplicated() ) {
268 // give the Interceptor a chance to process property values, if the properties
269 // were modified by the Interceptor, we need to set them back to the object
270 intercepted = handleInterception( event );
271 }
272 else {
273 intercepted = false;
274 }
275
276 validate( entity, persister, status, entityMode );
277
278 // increment the version number (if necessary)
279 final Object nextVersion = getNextVersion(event);
280
281 // if it was dirtied by a collection only
282 int[] dirtyProperties = event.getDirtyProperties();
283 if ( event.isDirtyCheckPossible() && dirtyProperties == null ) {
284 if ( ! intercepted && !event.hasDirtyCollection() ) {
285 throw new AssertionFailure( "dirty, but no dirty properties" );
286 }
287 dirtyProperties = ArrayHelper.EMPTY_INT_ARRAY;
288 }
289
290 // check nullability but do not perform command execute
291 // we'll use scheduled updates for that.
292 new Nullability(session).checkNullability( values, persister, true );
293
294 // schedule the update
295 // note that we intentionally do _not_ pass in currentPersistentState!
296 session.getActionQueue().addAction(
297 new EntityUpdateAction(
298 entry.getId(),
299 values,
300 dirtyProperties,
301 event.hasDirtyCollection(),
302 entry.getLoadedState(),
303 entry.getVersion(),
304 nextVersion,
305 entity,
306 entry.getRowId(),
307 persister,
308 session
309 )
310 );
311
312 return intercepted;
313 }
314
315 protected void validate(Object entity, EntityPersister persister, Status status, EntityMode entityMode) {
316 // validate() instances of Validatable
317 if ( status == Status.MANAGED && persister.implementsValidatable( entityMode ) ) {
318 ( (Validatable) entity ).validate();
319 }
320 }
321
322 protected boolean handleInterception(FlushEntityEvent event) {
323 SessionImplementor session = event.getSession();
324 EntityEntry entry = event.getEntityEntry();
325 EntityPersister persister = entry.getPersister();
326 Object entity = event.getEntity();
327
328 //give the Interceptor a chance to modify property values
329 final Object[] values = event.getPropertyValues();
330 final boolean intercepted = invokeInterceptor( session, entity, entry, values, persister );
331
332 //now we might need to recalculate the dirtyProperties array
333 if ( intercepted && event.isDirtyCheckPossible() && !event.isDirtyCheckHandledByInterceptor() ) {
334 int[] dirtyProperties;
335 if ( event.hasDatabaseSnapshot() ) {
336 dirtyProperties = persister.findModified( event.getDatabaseSnapshot(), values, entity, session );
337 }
338 else {
339 dirtyProperties = persister.findDirty( values, entry.getLoadedState(), entity, session );
340 }
341 event.setDirtyProperties(dirtyProperties);
342 }
343
344 return intercepted;
345 }
346
347 protected boolean invokeInterceptor(
348 SessionImplementor session,
349 Object entity,
350 EntityEntry entry,
351 final Object[] values,
352 EntityPersister persister) {
353 return session.getInterceptor().onFlushDirty(
354 entity,
355 entry.getId(),
356 values,
357 entry.getLoadedState(),
358 persister.getPropertyNames(),
359 persister.getPropertyTypes()
360 );
361 }
362
363 /**
364 * Convience method to retreive an entities next version value
365 */
366 private Object getNextVersion(FlushEntityEvent event) throws HibernateException {
367
368 EntityEntry entry = event.getEntityEntry();
369 EntityPersister persister = entry.getPersister();
370 if ( persister.isVersioned() ) {
371
372 Object[] values = event.getPropertyValues();
373
374 if ( entry.isBeingReplicated() ) {
375 return Versioning.getVersion(values, persister);
376 }
377 else {
378 int[] dirtyProperties = event.getDirtyProperties();
379
380 final boolean isVersionIncrementRequired = isVersionIncrementRequired(
381 event,
382 entry,
383 persister,
384 dirtyProperties
385 );
386
387 final Object nextVersion = isVersionIncrementRequired ?
388 Versioning.increment( entry.getVersion(), persister.getVersionType(), event.getSession() ) :
389 entry.getVersion(); //use the current version
390
391 Versioning.setVersion(values, nextVersion, persister);
392
393 return nextVersion;
394 }
395 }
396 else {
397 return null;
398 }
399
400 }
401
402 private boolean isVersionIncrementRequired(
403 FlushEntityEvent event,
404 EntityEntry entry,
405 EntityPersister persister,
406 int[] dirtyProperties
407 ) {
408 final boolean isVersionIncrementRequired = entry.getStatus()!=Status.DELETED && (
409 dirtyProperties==null ||
410 Versioning.isVersionIncrementRequired(
411 dirtyProperties,
412 event.hasDirtyCollection(),
413 persister.getPropertyVersionability()
414 )
415 );
416 return isVersionIncrementRequired;
417 }
418
419 /**
420 * Performs all necessary checking to determine if an entity needs an SQL update
421 * to synchronize its state to the database. Modifies the event by side-effect!
422 * Note: this method is quite slow, avoid calling if possible!
423 */
424 protected final boolean isUpdateNecessary(FlushEntityEvent event) throws HibernateException {
425
426 EntityPersister persister = event.getEntityEntry().getPersister();
427 Status status = event.getEntityEntry().getStatus();
428
429 if ( !event.isDirtyCheckPossible() ) {
430 return true;
431 }
432 else {
433
434 int[] dirtyProperties = event.getDirtyProperties();
435 if ( dirtyProperties!=null && dirtyProperties.length!=0 ) {
436 return true; //TODO: suck into event class
437 }
438 else {
439 return hasDirtyCollections( event, persister, status );
440 }
441
442 }
443 }
444
445 private boolean hasDirtyCollections(FlushEntityEvent event, EntityPersister persister, Status status) {
446 if ( isCollectionDirtyCheckNecessary(persister, status) ) {
447 DirtyCollectionSearchVisitor visitor = new DirtyCollectionSearchVisitor(
448 event.getSession(),
449 persister.getPropertyVersionability()
450 );
451 visitor.processEntityPropertyValues( event.getPropertyValues(), persister.getPropertyTypes() );
452 boolean hasDirtyCollections = visitor.wasDirtyCollectionFound();
453 event.setHasDirtyCollection(hasDirtyCollections);
454 return hasDirtyCollections;
455 }
456 else {
457 return false;
458 }
459 }
460
461 private boolean isCollectionDirtyCheckNecessary(EntityPersister persister, Status status) {
462 return status==Status.MANAGED &&
463 persister.isVersioned() &&
464 persister.hasCollections();
465 }
466
467 /**
468 * Perform a dirty check, and attach the results to the event
469 */
470 protected void dirtyCheck(FlushEntityEvent event) throws HibernateException {
471
472 final Object entity = event.getEntity();
473 final Object[] values = event.getPropertyValues();
474 final SessionImplementor session = event.getSession();
475 final EntityEntry entry = event.getEntityEntry();
476 final EntityPersister persister = entry.getPersister();
477 final Serializable id = entry.getId();
478 final Object[] loadedState = entry.getLoadedState();
479
480 int[] dirtyProperties = session.getInterceptor().findDirty(
481 entity,
482 id,
483 values,
484 loadedState,
485 persister.getPropertyNames(),
486 persister.getPropertyTypes()
487 );
488
489 event.setDatabaseSnapshot(null);
490
491 final boolean interceptorHandledDirtyCheck;
492 boolean cannotDirtyCheck;
493
494 if ( dirtyProperties==null ) {
495 // Interceptor returned null, so do the dirtycheck ourself, if possible
496 interceptorHandledDirtyCheck = false;
497
498 cannotDirtyCheck = loadedState==null; // object loaded by update()
499 if ( !cannotDirtyCheck ) {
500 // dirty check against the usual snapshot of the entity
501 dirtyProperties = persister.findDirty( values, loadedState, entity, session );
502
503 }
504 else {
505 // dirty check against the database snapshot, if possible/necessary
506 final Object[] databaseSnapshot = getDatabaseSnapshot(session, persister, id);
507 if ( databaseSnapshot != null ) {
508 dirtyProperties = persister.findModified(databaseSnapshot, values, entity, session);
509 cannotDirtyCheck = false;
510 event.setDatabaseSnapshot(databaseSnapshot);
511 }
512 }
513 }
514 else {
515 // the Interceptor handled the dirty checking
516 cannotDirtyCheck = false;
517 interceptorHandledDirtyCheck = true;
518 }
519
520 event.setDirtyProperties(dirtyProperties);
521 event.setDirtyCheckHandledByInterceptor(interceptorHandledDirtyCheck);
522 event.setDirtyCheckPossible(!cannotDirtyCheck);
523
524 }
525
526 private Object[] getDatabaseSnapshot(SessionImplementor session, EntityPersister persister, Serializable id) {
527 if ( persister.isSelectBeforeUpdateRequired() ) {
528 Object[] snapshot = session.getPersistenceContext()
529 .getDatabaseSnapshot(id, persister);
530 if (snapshot==null) {
531 //do we even really need this? the update will fail anyway....
532 if ( session.getFactory().getStatistics().isStatisticsEnabled() ) {
533 session.getFactory().getStatisticsImplementor()
534 .optimisticFailure( persister.getEntityName() );
535 }
536 throw new StaleObjectStateException( persister.getEntityName(), id );
537 }
538 else {
539 return snapshot;
540 }
541 }
542 else {
543 //TODO: optimize away this lookup for entities w/o unsaved-value="undefined"
544 EntityKey entityKey = new EntityKey( id, persister, session.getEntityMode() );
545 return session.getPersistenceContext()
546 .getCachedDatabaseSnapshot( entityKey );
547 }
548 }
549
550 }