Skip to content

Commit

Permalink
HHH-3910 - custom dirty flag tracking
Browse files Browse the repository at this point in the history
  • Loading branch information
sebersole committed Jan 24, 2012
1 parent aef0e25 commit 6258df4
Show file tree
Hide file tree
Showing 7 changed files with 339 additions and 9 deletions.
@@ -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);
}
Expand Up @@ -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";
}
Expand Up @@ -28,9 +28,11 @@
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;
Expand Down Expand Up @@ -269,12 +271,37 @@ public Object getLoadedValue(String propertyName) {
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;
}

/**
Expand All @@ -289,9 +316,9 @@ public boolean requiresDirtyCheck(Object entity) {
* @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) {
Expand Down
Expand Up @@ -27,6 +27,7 @@
import java.util.Properties;
import java.util.Set;

import org.hibernate.CustomEntityDirtinessStrategy;
import org.hibernate.HibernateException;
import org.hibernate.Interceptor;
import org.hibernate.MappingException;
Expand Down Expand Up @@ -238,4 +239,6 @@ public interface SessionFactoryImplementor extends Mapping, SessionFactory {
public ServiceRegistryImplementor getServiceRegistry();

public void addObserver(SessionFactoryObserver observer);

public CustomEntityDirtinessStrategy getCustomEntityDirtinessStrategy();
}
Expand Up @@ -47,6 +47,7 @@
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;
Expand All @@ -72,6 +73,7 @@
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;
Expand Down Expand Up @@ -122,6 +124,7 @@
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;
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -531,10 +535,64 @@ public void sessionFactoryClosed(SessionFactory factory) {
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,
Expand Down Expand Up @@ -853,6 +911,7 @@ public void sessionFactoryClosed(SessionFactory factory) {
fetchProfiles.put( fetchProfile.getName(), fetchProfile );
}

this.customEntityDirtinessStrategy = determineCustomEntityDirtinessStrategy( properties );
this.transactionEnvironment = new TransactionEnvironmentImpl( this );
this.observer.sessionFactoryCreated( this );
}
Expand Down Expand Up @@ -1723,6 +1782,10 @@ public StatelessSessionBuilder tenantIdentifier(String tenantIdentifier) {
}
}

public CustomEntityDirtinessStrategy getCustomEntityDirtinessStrategy() {
return customEntityDirtinessStrategy;
}


// Serialization handling ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

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

}

0 comments on commit 6258df4

Please sign in to comment.