From 91847d702717910c2f7ed98b478562d6f8b7557a Mon Sep 17 00:00:00 2001 From: Steve Ebersole Date: Fri, 27 Jan 2012 14:51:01 -0600 Subject: [PATCH] HHH-6998 - Expand CustomEntityDirtinessStrategy to define findDirty --- .../CustomEntityDirtinessStrategy.java | 112 +++++++++++++++++- .../org/hibernate/engine/spi/EntityEntry.java | 13 +- .../DefaultFlushEntityEventListener.java | 91 +++++++++++++- .../internal/SessionFactoryImpl.java | 10 +- .../CustomDirtinessStrategyTest.java | 106 ++++++++++++++--- .../org/hibernate/test/dirtiness/Thing.java | 9 ++ 6 files changed, 312 insertions(+), 29 deletions(-) diff --git a/hibernate-core/src/main/java/org/hibernate/CustomEntityDirtinessStrategy.java b/hibernate-core/src/main/java/org/hibernate/CustomEntityDirtinessStrategy.java index 071b3163af..dbec870c1b 100644 --- a/hibernate-core/src/main/java/org/hibernate/CustomEntityDirtinessStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/CustomEntityDirtinessStrategy.java @@ -23,6 +23,9 @@ */ package org.hibernate; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.Type; + /** * During a flush cycle, Hibernate needs to determine which of the entities associated with a {@link Session}. * Dirty entities are the ones that get {@literal UPDATE}ed to the database. @@ -40,29 +43,132 @@ public interface CustomEntityDirtinessStrategy { * {@link #isDirty} will be called next as the definitive means to determine whether the entity is dirty. * * @param entity The entity to be check. + * @param persister The persister corresponding to the given entity * @param session The session from which this check originates. * * @return {@code true} indicates the dirty check can be done; {@code false} indicates it cannot. */ - public boolean canDirtyCheck(Object entity, Session session); + public boolean canDirtyCheck(Object entity, EntityPersister persister, Session session); /** * The callback used by Hibernate to determine if the given entity is dirty. Only called if the previous * {@link #canDirtyCheck} returned {@code true} * * @param entity The entity to check. + * @param persister The persister corresponding to the given entity * @param session The session from which this check originates. * * @return {@code true} indicates the entity is dirty; {@link false} indicates the entity is not dirty. */ - public boolean isDirty(Object entity, Session session); + public boolean isDirty(Object entity, EntityPersister persister, Session session); /** * Callback used by Hibernate to signal that the entity dirty flag should be cleared. Generally this * happens after previous dirty changes were written to the database. * * @param entity The entity to reset + * @param persister The persister corresponding to the given entity * @param session The session from which this call originates. */ - public void resetDirty(Object entity, Session session); + public void resetDirty(Object entity, EntityPersister persister, Session session); + + /** + * Callback used to hook into Hibernate algorithm for determination of which attributes have changed. Applications + * wanting to hook in to this would call back into the given {@link DirtyCheckContext#doDirtyChecking} + * method passing along an appropriate {@link AttributeChecker} implementation. + * + * @param entity The entity being checked + * @param persister The persister corresponding to the given entity + * @param session The session from which this call originates. + * @param dirtyCheckContext The callback context + */ + public void findDirty(Object entity, EntityPersister persister, Session session, DirtyCheckContext dirtyCheckContext); + + /** + * A callback to drive dirty checking. Handed to the {@link CustomEntityDirtinessStrategy#findDirty} method + * so that it can callback on to it if it wants to handle dirty checking rather than using Hibernate's default + * checking + * + * @see CustomEntityDirtinessStrategy#findDirty + */ + public static interface DirtyCheckContext { + /** + * The callback to indicate that dirty checking (the dirty attribute determination phase) should be handled + * by the calling {@link CustomEntityDirtinessStrategy} using the given {@link AttributeChecker} + * + * @param attributeChecker The delegate usable by the context for determining which attributes are dirty. + */ + public void doDirtyChecking(AttributeChecker attributeChecker); + } + + /** + * Responsible for identifying when attributes are dirty. + */ + public static interface AttributeChecker { + /** + * Do the attribute dirty check. + * + * @param attributeInformation Information about the attribute which is useful to help determine if it is + * dirty. + * + * @return {@code true} indicates the attribute value has changed; {@code false} indicates it has not. + */ + public boolean isDirty(AttributeInformation attributeInformation); + } + + /** + * Provides {@link AttributeChecker} with meta information about the attributes being checked. + */ + @SuppressWarnings( {"UnusedDeclaration"}) + public static interface AttributeInformation { + /** + * Get a reference to the persister for the entity containing this attribute. + * + * @return The entity persister. + */ + public EntityPersister getContainingPersister(); + + /** + * Many of Hibernate internals use arrays to define information about attributes. This value + * provides this index into those arrays for this particular attribute. + *

+ * It can be useful if needing to leverage those Hibernate internals. + * + * @return The attribute index. + */ + public int getAttributeIndex(); + + /** + * Get the name of this attribute + * + * @return The attribute name + */ + public String getName(); + + /** + * Get the mapping type of this attribute. + * + * @return The mapping type. + */ + public Type getType(); + + /** + * Get the current value of this attribute. + * + * @return The attributes current value + */ + public Object getCurrentValue(); + + /** + * Get the loaded value of this attribute. + *

+ * NOTE : A call to this method may require hitting the database in cases where the loaded state is + * not known. In those cases the db hit is incurred only once per entity, not for each attribute. + * + * @return The attributes loaded value + */ + public Object getLoadedValue(); + } + + } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntry.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntry.java index baafc975a7..d98d5127e5 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntry.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/EntityEntry.java @@ -229,16 +229,17 @@ public final class EntityEntry implements Serializable { this.version = nextVersion; getPersister().setPropertyValue( entity, getPersister().getVersionProperty(), nextVersion ); } + if ( getPersister().getInstrumentationMetadata().isInstrumented() ) { final FieldInterceptor interceptor = getPersister().getInstrumentationMetadata().extractInterceptor( entity ); if ( interceptor != null ) { interceptor.clearDirty(); } - persistenceContext.getSession() - .getFactory() - .getCustomEntityDirtinessStrategy() - .resetDirty( entity, (Session) persistenceContext.getSession() ); } + persistenceContext.getSession() + .getFactory() + .getCustomEntityDirtinessStrategy() + .resetDirty( entity, getPersister(), (Session) persistenceContext.getSession() ); notifyLoadedStateUpdated(); } @@ -301,8 +302,8 @@ public final class EntityEntry implements Serializable { final CustomEntityDirtinessStrategy customEntityDirtinessStrategy = persistenceContext.getSession().getFactory().getCustomEntityDirtinessStrategy(); - if ( customEntityDirtinessStrategy.canDirtyCheck( entity, (Session) persistenceContext.getSession() ) ) { - return ! customEntityDirtinessStrategy.isDirty( entity, (Session) persistenceContext.getSession() ); + if ( customEntityDirtinessStrategy.canDirtyCheck( entity, getPersister(), (Session) persistenceContext.getSession() ) ) { + return ! customEntityDirtinessStrategy.isDirty( entity, getPersister(), (Session) persistenceContext.getSession() ); } return false; diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java index e100ded87d..ae2445d85c 100755 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java @@ -24,11 +24,14 @@ package org.hibernate.event.internal; import java.io.Serializable; +import java.util.Arrays; import org.jboss.logging.Logger; import org.hibernate.AssertionFailure; +import org.hibernate.CustomEntityDirtinessStrategy; import org.hibernate.HibernateException; +import org.hibernate.Session; import org.hibernate.StaleObjectStateException; import org.hibernate.action.internal.DelayedPostInsertIdentifier; import org.hibernate.action.internal.EntityUpdateAction; @@ -252,7 +255,7 @@ public class DefaultFlushEntityEventListener implements FlushEntityEventListener event.getSession() .getFactory() .getCustomEntityDirtinessStrategy() - .resetDirty( event.getEntity(), event.getSession() ); + .resetDirty( event.getEntity(), event.getEntityEntry().getPersister(), event.getSession() ); return false; } } @@ -477,7 +480,7 @@ public class DefaultFlushEntityEventListener implements FlushEntityEventListener /** * Perform a dirty check, and attach the results to the event */ - protected void dirtyCheck(FlushEntityEvent event) throws HibernateException { + protected void dirtyCheck(final FlushEntityEvent event) throws HibernateException { final Object entity = event.getEntity(); final Object[] values = event.getPropertyValues(); @@ -494,7 +497,29 @@ public class DefaultFlushEntityEventListener implements FlushEntityEventListener loadedState, persister.getPropertyNames(), persister.getPropertyTypes() + ); + + if ( dirtyProperties == null ) { + // see if the custom dirtiness strategy can tell us... + class DirtyCheckContextImpl implements CustomEntityDirtinessStrategy.DirtyCheckContext { + int[] found = null; + @Override + public void doDirtyChecking(CustomEntityDirtinessStrategy.AttributeChecker attributeChecker) { + found = new DirtyCheckAttributeInfoImpl( event ).visitAttributes( attributeChecker ); + if ( found != null && found.length == 0 ) { + found = null; + } + } + } + DirtyCheckContextImpl context = new DirtyCheckContextImpl(); + session.getFactory().getCustomEntityDirtinessStrategy().findDirty( + entity, + persister, + (Session) session, + context ); + dirtyProperties = context.found; + } event.setDatabaseSnapshot(null); @@ -554,6 +579,68 @@ public class DefaultFlushEntityEventListener implements FlushEntityEventListener } + private class DirtyCheckAttributeInfoImpl implements CustomEntityDirtinessStrategy.AttributeInformation { + private final FlushEntityEvent event; + private final EntityPersister persister; + private final int numberOfAttributes; + private int index = 0; + + private DirtyCheckAttributeInfoImpl(FlushEntityEvent event) { + this.event = event; + this.persister = event.getEntityEntry().getPersister(); + this.numberOfAttributes = persister.getPropertyNames().length; + } + + @Override + public EntityPersister getContainingPersister() { + return persister; + } + + @Override + public int getAttributeIndex() { + return index; + } + + @Override + public String getName() { + return persister.getPropertyNames()[ index ]; + } + + @Override + public Type getType() { + return persister.getPropertyTypes()[ index ]; + } + + @Override + public Object getCurrentValue() { + return event.getPropertyValues()[ index ]; + } + + Object[] databaseSnapshot; + + @Override + public Object getLoadedValue() { + if ( databaseSnapshot == null ) { + databaseSnapshot = getDatabaseSnapshot( event.getSession(), persister, event.getEntityEntry().getId() ); + } + return databaseSnapshot[ index ]; + } + + public int[] visitAttributes(CustomEntityDirtinessStrategy.AttributeChecker attributeChecker) { + databaseSnapshot = null; + index = 0; + + final int[] indexes = new int[ numberOfAttributes ]; + int count = 0; + for ( ; index < numberOfAttributes; index++ ) { + if ( attributeChecker.isDirty( this ) ) { + indexes[ count++ ] = index; + } + } + return Arrays.copyOf( indexes, count ); + } + } + private void logDirtyProperties(Serializable id, int[] dirtyProperties, EntityPersister persister) { if ( LOG.isTraceEnabled() && dirtyProperties != null && dirtyProperties.length > 0 ) { final String[] allPropertyNames = persister.getPropertyNames(); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java index 060927a4f9..d1964f3fb5 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java @@ -578,17 +578,21 @@ public final class SessionFactoryImpl // last resort return new CustomEntityDirtinessStrategy() { @Override - public boolean canDirtyCheck(Object entity, Session session) { + public boolean canDirtyCheck(Object entity, EntityPersister persister, Session session) { return false; } @Override - public boolean isDirty(Object entity, Session session) { + public boolean isDirty(Object entity, EntityPersister persister, Session session) { return false; } @Override - public void resetDirty(Object entity, Session session) { + public void resetDirty(Object entity, EntityPersister persister, Session session) { + } + + @Override + public void findDirty(Object entity, DirtyCheckContext dirtyCheckContext) { } }; } diff --git a/hibernate-core/src/test/java/org/hibernate/test/dirtiness/CustomDirtinessStrategyTest.java b/hibernate-core/src/test/java/org/hibernate/test/dirtiness/CustomDirtinessStrategyTest.java index f9d5869530..b95fc045a5 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/dirtiness/CustomDirtinessStrategyTest.java +++ b/hibernate-core/src/test/java/org/hibernate/test/dirtiness/CustomDirtinessStrategyTest.java @@ -27,17 +27,24 @@ import org.hibernate.CustomEntityDirtinessStrategy; import org.hibernate.Session; import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.Configuration; +import org.hibernate.persister.entity.EntityPersister; +import org.junit.Before; import org.junit.Test; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; /** * @author Steve Ebersole */ public class CustomDirtinessStrategyTest extends BaseCoreFunctionalTestCase { + private static final String INITIAL_NAME = "thing 1"; + private static final String SUBSEQUENT_NAME = "thing 2"; + @Override protected void configure(Configuration configuration) { super.configure( configuration ); @@ -50,47 +57,116 @@ public class CustomDirtinessStrategyTest extends BaseCoreFunctionalTestCase { } @Test - public void testCustomStrategy() throws Exception { - final String INITIAL_NAME = "thing 1"; - final String SUBSEQUENT_NAME = "thing 2"; - + public void testOnlyCustomStrategy() { Session session = openSession(); session.beginTransaction(); - session.save( new Thing( INITIAL_NAME ) ); + Long id = (Long) session.save( new Thing( INITIAL_NAME ) ); session.getTransaction().commit(); session.close(); + Strategy.INSTANCE.resetState(); + session = openSession(); session.beginTransaction(); - Thing thing = (Thing) session.get( Thing.class, 1L ); + Thing thing = (Thing) session.get( Thing.class, id ); thing.setName( SUBSEQUENT_NAME ); session.getTransaction().commit(); session.close(); + assertEquals( 1, Strategy.INSTANCE.canDirtyCheckCount ); + assertEquals( 1, Strategy.INSTANCE.isDirtyCount ); + assertEquals( 1, Strategy.INSTANCE.resetDirtyCount ); + assertEquals( 1, Strategy.INSTANCE.findDirtyCount ); + session = openSession(); session.beginTransaction(); - thing = (Thing) session.get( Thing.class, 1L ); - assertEquals( INITIAL_NAME, thing.getName() ); + thing = (Thing) session.get( Thing.class, id ); + assertEquals( SUBSEQUENT_NAME, thing.getName() ); session.delete( thing ); session.getTransaction().commit(); session.close(); } + @Test + public void testOnlyCustomStrategyConsultedOnNonDirty() throws Exception { + Session session = openSession(); + session.beginTransaction(); + Long id = (Long) session.save( new Thing( INITIAL_NAME ) ); + session.getTransaction().commit(); + session.close(); + + session = openSession(); + session.beginTransaction(); + Thing thing = (Thing) session.get( Thing.class, id ); + // lets change the name + thing.setName( SUBSEQUENT_NAME ); + assertTrue( Strategy.INSTANCE.isDirty( thing, null, null ) ); + // but fool the dirty map + thing.changedValues.clear(); + assertFalse( Strategy.INSTANCE.isDirty( thing, null, null ) ); + session.getTransaction().commit(); + session.close(); + + session = openSession(); + session.beginTransaction(); + thing = (Thing) session.get( Thing.class, id ); + assertEquals( INITIAL_NAME, thing.getName() ); + session.createQuery( "delete Thing" ).executeUpdate(); + session.getTransaction().commit(); + session.close(); + } + public static class Strategy implements CustomEntityDirtinessStrategy { public static final Strategy INSTANCE = new Strategy(); - @Override - public boolean canDirtyCheck(Object entity, Session session) { - return true; - } + int canDirtyCheckCount = 0; @Override - public boolean isDirty(Object entity, Session session) { - return false; + public boolean canDirtyCheck(Object entity, EntityPersister persister, Session session) { + canDirtyCheckCount++; + System.out.println( "canDirtyCheck called" ); + return Thing.class.isInstance( entity ); } + int isDirtyCount = 0; + @Override - public void resetDirty(Object entity, Session session) { + public boolean isDirty(Object entity, EntityPersister persister, Session session) { + isDirtyCount++; + System.out.println( "isDirty called" ); + return ! Thing.class.cast( entity ).changedValues.isEmpty(); + } + + int resetDirtyCount = 0; + + @Override + public void resetDirty(Object entity, EntityPersister persister, Session session) { + resetDirtyCount++; + System.out.println( "resetDirty called" ); + Thing.class.cast( entity ).changedValues.clear(); + } + + int findDirtyCount = 0; + + @Override + public void findDirty(final Object entity, EntityPersister persister, Session session, DirtyCheckContext dirtyCheckContext) { + findDirtyCount++; + System.out.println( "findDirty called" ); + dirtyCheckContext.doDirtyChecking( + new AttributeChecker() { + @Override + public boolean isDirty(AttributeInformation attributeInformation) { + return Thing.class.cast( entity ).changedValues.containsKey( attributeInformation.getName() ); + } + } + ); + } + + void resetState() { + canDirtyCheckCount = 0; + isDirtyCount = 0; + resetDirtyCount = 0; + findDirtyCount = 0; } } diff --git a/hibernate-core/src/test/java/org/hibernate/test/dirtiness/Thing.java b/hibernate-core/src/test/java/org/hibernate/test/dirtiness/Thing.java index f14056791d..14d36afd7d 100644 --- a/hibernate-core/src/test/java/org/hibernate/test/dirtiness/Thing.java +++ b/hibernate-core/src/test/java/org/hibernate/test/dirtiness/Thing.java @@ -26,6 +26,10 @@ package org.hibernate.test.dirtiness; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; +import javax.persistence.Transient; + +import java.util.HashMap; +import java.util.Map; import org.hibernate.annotations.GenericGenerator; @@ -61,6 +65,11 @@ public class Thing { } public void setName(String name) { + // intentionally simple dirty tracking (i.e. no checking against previous state) + changedValues.put( "name", this.name ); this.name = name; } + + @Transient + Map changedValues = new HashMap(); }