HHH-6998 - Expand CustomEntityDirtinessStrategy to define findDirty
This commit is contained in:
parent
ebee6b731e
commit
91847d7027
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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) {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>();
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue