HHH-3910 - custom dirty flag tracking

This commit is contained in:
Steve Ebersole 2012-01-23 22:50:35 -06:00
parent aef0e25405
commit 6258df4752
7 changed files with 339 additions and 9 deletions

View File

@ -0,0 +1,68 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2012, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate;
/**
* 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.
* <p/>
* In some circumstances, that process of determining whether an entity is dirty can take a significant time as
* by default Hibernate must check each of the entity's attribute values one-by-one. Oftentimes applications
* already have knowledge of an entity's dirtiness and using that information instead would be more performant.
* The purpose of this contract then is to allow applications such a plug-in point.
*
* @author Steve Ebersole
*/
public interface CustomEntityDirtinessStrategy {
/**
* Is this strategy capable of telling whether the given entity is dirty? A return of {@code true} means that
* {@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 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);
/**
* 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 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);
/**
* 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 session The session from which this call originates.
*/
public void resetDirty(Object entity, Session session);
}

View File

@ -540,4 +540,10 @@ public interface AvailableSettings {
* Default to false to keep backward compatibility.
*/
public static final String USE_NEW_ID_GENERATOR_MAPPINGS = "hibernate.id.new_generator_mappings";
/**
* Setting to identify a {@link org.hibernate.CustomEntityDirtinessStrategy} to use. May point to
* either a class name or instance.
*/
public static final String CUSTOM_ENTITY_DIRTINESS_STRATEGY = "hibernate.entity_dirtiness_strategy";
}

View File

@ -28,9 +28,11 @@ import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import org.hibernate.CustomEntityDirtinessStrategy;
import org.hibernate.EntityMode;
import org.hibernate.HibernateException;
import org.hibernate.LockMode;
import org.hibernate.Session;
import org.hibernate.bytecode.instrumentation.spi.FieldInterceptor;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.persister.entity.UniqueKeyLoadable;
@ -269,12 +271,37 @@ public final class EntityEntry implements Serializable {
return loadedState[propertyIndex];
}
public boolean requiresDirtyCheck(Object entity) {
return isModifiableEntity() && (
getPersister().hasMutableProperties() ||
!getPersister().getInstrumentationMetadata().isInstrumented() ||
getPersister().getInstrumentationMetadata().extractInterceptor( entity ).isDirty()
);
/**
* Not sure this is the best method name, but the general idea here is to return {@code true} if the entity can
* possibly be dirty. This can only be the case if it is in a modifiable state (not read-only/deleted) and it
* either has mutable properties or field-interception is not telling us it is dirty. Clear as mud? :/
*
* A name like canPossiblyBeDirty might be better
*
* @param entity The entity to test
*
* @return {@code true} indicates that the entity could possibly be dirty and that dirty check
* should happen; {@code false} indicates there is no way the entity can be dirty
*/
public boolean requiresDirtyCheck(Object entity) {
return isModifiableEntity()
&& ( getPersister().hasMutableProperties() || ! isUnequivocallyNonDirty( entity ) );
}
@SuppressWarnings( {"SimplifiableIfStatement"})
private boolean isUnequivocallyNonDirty(Object entity) {
if ( getPersister().getInstrumentationMetadata().isInstrumented() ) {
// the entity must be instrumented (otherwise we cant check dirty flag) and the dirty flag is false
return ! getPersister().getInstrumentationMetadata().extractInterceptor( entity ).isDirty();
}
final CustomEntityDirtinessStrategy customEntityDirtinessStrategy =
persistenceContext.getSession().getFactory().getCustomEntityDirtinessStrategy();
if ( customEntityDirtinessStrategy.canDirtyCheck( entity, (Session) persistenceContext.getSession() ) ) {
return ! customEntityDirtinessStrategy.isDirty( entity, (Session) persistenceContext.getSession() );
}
return false;
}
/**
@ -289,9 +316,9 @@ public final class EntityEntry implements Serializable {
* @return true, if the entity is modifiable; false, otherwise,
*/
public boolean isModifiableEntity() {
return ( status != Status.READ_ONLY ) &&
! ( status == Status.DELETED && previousStatus == Status.READ_ONLY ) &&
getPersister().isMutable();
return getPersister().isMutable()
&& status != Status.READ_ONLY
&& ! ( status == Status.DELETED && previousStatus == Status.READ_ONLY );
}
public void forceLocked(Object entity, Object nextVersion) {

View File

@ -27,6 +27,7 @@ import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.hibernate.CustomEntityDirtinessStrategy;
import org.hibernate.HibernateException;
import org.hibernate.Interceptor;
import org.hibernate.MappingException;
@ -238,4 +239,6 @@ public interface SessionFactoryImplementor extends Mapping, SessionFactory {
public ServiceRegistryImplementor getServiceRegistry();
public void addObserver(SessionFactoryObserver observer);
public CustomEntityDirtinessStrategy getCustomEntityDirtinessStrategy();
}

View File

@ -47,6 +47,7 @@ import org.jboss.logging.Logger;
import org.hibernate.AssertionFailure;
import org.hibernate.Cache;
import org.hibernate.ConnectionReleaseMode;
import org.hibernate.CustomEntityDirtinessStrategy;
import org.hibernate.EmptyInterceptor;
import org.hibernate.EntityNameResolver;
import org.hibernate.HibernateException;
@ -72,6 +73,7 @@ import org.hibernate.cache.spi.access.AccessType;
import org.hibernate.cache.spi.access.CollectionRegionAccessStrategy;
import org.hibernate.cache.spi.access.EntityRegionAccessStrategy;
import org.hibernate.cache.spi.access.RegionAccessStrategy;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration;
import org.hibernate.cfg.Environment;
import org.hibernate.cfg.Settings;
@ -122,6 +124,7 @@ import org.hibernate.persister.spi.PersisterFactory;
import org.hibernate.pretty.MessageHelper;
import org.hibernate.proxy.EntityNotFoundDelegate;
import org.hibernate.service.ServiceRegistry;
import org.hibernate.service.classloading.spi.ClassLoaderService;
import org.hibernate.service.config.spi.ConfigurationService;
import org.hibernate.service.jdbc.connections.spi.ConnectionProvider;
import org.hibernate.service.jndi.spi.JndiService;
@ -206,6 +209,7 @@ public final class SessionFactoryImpl
private final transient TypeHelper typeHelper;
private final transient TransactionEnvironment transactionEnvironment;
private final transient SessionFactoryOptions sessionFactoryOptions;
private final transient CustomEntityDirtinessStrategy customEntityDirtinessStrategy;
@SuppressWarnings( {"unchecked", "ThrowableResultOfMethodCallIgnored"})
public SessionFactoryImpl(
@ -531,10 +535,64 @@ public final class SessionFactoryImpl
fetchProfiles.put( fetchProfile.getName(), fetchProfile );
}
this.customEntityDirtinessStrategy = determineCustomEntityDirtinessStrategy( properties );
this.transactionEnvironment = new TransactionEnvironmentImpl( this );
this.observer.sessionFactoryCreated( this );
}
@SuppressWarnings( {"unchecked"})
private CustomEntityDirtinessStrategy determineCustomEntityDirtinessStrategy(Properties properties) {
final Object value = properties.get( AvailableSettings.CUSTOM_ENTITY_DIRTINESS_STRATEGY );
if ( value != null ) {
if ( CustomEntityDirtinessStrategy.class.isInstance( value ) ) {
return CustomEntityDirtinessStrategy.class.cast( value );
}
Class<CustomEntityDirtinessStrategy> customEntityDirtinessStrategyClass;
if ( Class.class.isInstance( value ) ) {
customEntityDirtinessStrategyClass = Class.class.cast( customEntityDirtinessStrategy );
}
else {
try {
customEntityDirtinessStrategyClass = serviceRegistry.getService( ClassLoaderService.class )
.classForName( value.toString() );
}
catch (Exception e) {
LOG.debugf(
"Unable to locate CustomEntityDirtinessStrategy implementation class %s",
value.toString()
);
customEntityDirtinessStrategyClass = null;
}
}
try {
return customEntityDirtinessStrategyClass.newInstance();
}
catch (Exception e) {
LOG.debugf(
"Unable to instantiate CustomEntityDirtinessStrategy class %s",
customEntityDirtinessStrategyClass.getName()
);
}
}
// last resort
return new CustomEntityDirtinessStrategy() {
@Override
public boolean canDirtyCheck(Object entity, Session session) {
return false;
}
@Override
public boolean isDirty(Object entity, Session session) {
return false;
}
@Override
public void resetDirty(Object entity, Session session) {
}
};
}
@SuppressWarnings( {"ThrowableResultOfMethodCallIgnored"})
public SessionFactoryImpl(
MetadataImplementor metadata,
@ -853,6 +911,7 @@ public final class SessionFactoryImpl
fetchProfiles.put( fetchProfile.getName(), fetchProfile );
}
this.customEntityDirtinessStrategy = determineCustomEntityDirtinessStrategy( properties );
this.transactionEnvironment = new TransactionEnvironmentImpl( this );
this.observer.sessionFactoryCreated( this );
}
@ -1723,6 +1782,10 @@ public final class SessionFactoryImpl
}
}
public CustomEntityDirtinessStrategy getCustomEntityDirtinessStrategy() {
return customEntityDirtinessStrategy;
}
// Serialization handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -0,0 +1,97 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2012, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.test.dirtiness;
import org.hibernate.CustomEntityDirtinessStrategy;
import org.hibernate.Session;
import org.hibernate.cfg.AvailableSettings;
import org.hibernate.cfg.Configuration;
import org.junit.Test;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import static org.junit.Assert.assertEquals;
/**
* @author Steve Ebersole
*/
public class CustomDirtinessStrategyTest extends BaseCoreFunctionalTestCase {
@Override
protected void configure(Configuration configuration) {
super.configure( configuration );
configuration.getProperties().put( AvailableSettings.CUSTOM_ENTITY_DIRTINESS_STRATEGY, Strategy.INSTANCE );
}
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class[] { Thing.class };
}
@Test
public void testCustomStrategy() throws Exception {
final String INITIAL_NAME = "thing 1";
final String SUBSEQUENT_NAME = "thing 2";
Session session = openSession();
session.beginTransaction();
session.save( new Thing( INITIAL_NAME ) );
session.getTransaction().commit();
session.close();
session = openSession();
session.beginTransaction();
Thing thing = (Thing) session.get( Thing.class, 1L );
thing.setName( SUBSEQUENT_NAME );
session.getTransaction().commit();
session.close();
session = openSession();
session.beginTransaction();
thing = (Thing) session.get( Thing.class, 1L );
assertEquals( INITIAL_NAME, thing.getName() );
session.delete( thing );
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;
}
@Override
public boolean isDirty(Object entity, Session session) {
return false;
}
@Override
public void resetDirty(Object entity, Session session) {
}
}
}

View File

@ -0,0 +1,66 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* Copyright (c) 2012, Red Hat Inc. or third-party contributors as
* indicated by the @author tags or express copyright attribution
* statements applied by the authors. All third-party contributions are
* distributed under license by Red Hat Inc.
*
* This copyrighted material is made available to anyone wishing to use, modify,
* copy, or redistribute it subject to the terms and conditions of the GNU
* Lesser General Public License, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
* for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this distribution; if not, write to:
* Free Software Foundation, Inc.
* 51 Franklin Street, Fifth Floor
* Boston, MA 02110-1301 USA
*/
package org.hibernate.test.dirtiness;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import org.hibernate.annotations.GenericGenerator;
/**
* @author Steve Ebersole
*/
@Entity
public class Thing {
@Id
@GeneratedValue( generator = "increment" )
@GenericGenerator( strategy = "increment", name = "increment" )
private Long id;
private String name;
public Thing() {
}
public Thing(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}