HHH-6998 - Expand CustomEntityDirtinessStrategy to define findDirty

This commit is contained in:
Steve Ebersole 2012-01-27 14:51:01 -06:00
parent ebee6b731e
commit 91847d7027
6 changed files with 312 additions and 29 deletions

View File

@ -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.
* <p/>
* 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.
* <p/>
* <b>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.</b>
*
* @return The attributes loaded value
*/
public Object getLoadedValue();
}
}

View File

@ -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;

View File

@ -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();

View File

@ -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) {
}
};
}

View File

@ -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;
}
}

View File

@ -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<String,Object> changedValues = new HashMap<String, Object>();
}