HHH-8973 - Fix tracking entity modifications to detached entities with the modified flags feature.

This commit is contained in:
Chris Cranford 2017-01-14 15:57:52 -05:00
parent ad0cd4fc18
commit ebf4803a33
13 changed files with 189 additions and 5 deletions

View File

@ -18,6 +18,7 @@ import org.hibernate.envers.event.spi.EnversPostInsertEventListenerImpl;
import org.hibernate.envers.event.spi.EnversPostUpdateEventListenerImpl; import org.hibernate.envers.event.spi.EnversPostUpdateEventListenerImpl;
import org.hibernate.envers.event.spi.EnversPreCollectionRemoveEventListenerImpl; import org.hibernate.envers.event.spi.EnversPreCollectionRemoveEventListenerImpl;
import org.hibernate.envers.event.spi.EnversPreCollectionUpdateEventListenerImpl; import org.hibernate.envers.event.spi.EnversPreCollectionUpdateEventListenerImpl;
import org.hibernate.envers.event.spi.EnversPreUpdateEventListenerImpl;
import org.hibernate.event.service.spi.EventListenerRegistry; import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType; import org.hibernate.event.spi.EventType;
import org.hibernate.integrator.spi.Integrator; import org.hibernate.integrator.spi.Integrator;
@ -29,6 +30,7 @@ import org.jboss.logging.Logger;
* Hooks up Envers event listeners. * Hooks up Envers event listeners.
* *
* @author Steve Ebersole * @author Steve Ebersole
* @author Chris Cranford
*/ */
public class EnversIntegrator implements Integrator { public class EnversIntegrator implements Integrator {
private static final Logger log = Logger.getLogger( EnversIntegrator.class ); private static final Logger log = Logger.getLogger( EnversIntegrator.class );
@ -89,6 +91,10 @@ public class EnversIntegrator implements Integrator {
EventType.POST_INSERT, EventType.POST_INSERT,
new EnversPostInsertEventListenerImpl( enversService ) new EnversPostInsertEventListenerImpl( enversService )
); );
listenerRegistry.appendListeners(
EventType.PRE_UPDATE,
new EnversPreUpdateEventListenerImpl( enversService )
);
listenerRegistry.appendListeners( listenerRegistry.appendListeners(
EventType.POST_UPDATE, EventType.POST_UPDATE,
new EnversPostUpdateEventListenerImpl( enversService ) new EnversPostUpdateEventListenerImpl( enversService )

View File

@ -0,0 +1,35 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.envers.event.spi;
import org.hibernate.envers.boot.internal.EnversService;
import org.hibernate.envers.internal.entities.EntityConfiguration;
/**
* @author Chris Cranford
*/
public abstract class BaseEnversUpdateEventListener extends BaseEnversEventListener {
public BaseEnversUpdateEventListener(EnversService enversService) {
super( enversService );
}
/**
* Returns whether the entity has {@code withModifiedFlag} features and has no old state, most likely implying
* it was updated in a detached entity state.
*
* @param entityName The associated entity name.
* @param oldState The event old (likely detached) entity state.
* @return {@code true} if the entity is/has been updated in detached state, otherwise {@code false}.
*/
protected boolean isDetachedEntityUpdate(String entityName, Object[] oldState) {
final EntityConfiguration configuration = getEnversService().getEntitiesConfigurations().get( entityName );
if ( configuration.getPropertyMapper() != null && oldState == null ) {
return configuration.getPropertyMapper().hasPropertiesWithModifiedFlag();
}
return false;
}
}

View File

@ -20,8 +20,9 @@ import org.hibernate.persister.entity.EntityPersister;
* @author Adam Warski (adam at warski dot org) * @author Adam Warski (adam at warski dot org)
* @author HernпїЅn Chanfreau * @author HernпїЅn Chanfreau
* @author Steve Ebersole * @author Steve Ebersole
* @author Chris Cranford
*/ */
public class EnversPostUpdateEventListenerImpl extends BaseEnversEventListener implements PostUpdateEventListener { public class EnversPostUpdateEventListenerImpl extends BaseEnversUpdateEventListener implements PostUpdateEventListener {
public EnversPostUpdateEventListenerImpl(EnversService enversService) { public EnversPostUpdateEventListenerImpl(EnversService enversService) {
super( enversService ); super( enversService );
} }
@ -34,6 +35,8 @@ public class EnversPostUpdateEventListenerImpl extends BaseEnversEventListener i
checkIfTransactionInProgress( event.getSession() ); checkIfTransactionInProgress( event.getSession() );
final AuditProcess auditProcess = getEnversService().getAuditProcessManager().get( event.getSession() ); final AuditProcess auditProcess = getEnversService().getAuditProcessManager().get( event.getSession() );
Object[] oldState = getOldDBState( auditProcess, entityName, event );
final Object[] newDbState = postUpdateDBState( event ); final Object[] newDbState = postUpdateDBState( event );
final AuditWorkUnit workUnit = new ModWorkUnit( final AuditWorkUnit workUnit = new ModWorkUnit(
event.getSession(), event.getSession(),
@ -42,7 +45,7 @@ public class EnversPostUpdateEventListenerImpl extends BaseEnversEventListener i
event.getId(), event.getId(),
event.getPersister(), event.getPersister(),
newDbState, newDbState,
event.getOldState() oldState
); );
auditProcess.addWorkUnit( workUnit ); auditProcess.addWorkUnit( workUnit );
@ -52,13 +55,20 @@ public class EnversPostUpdateEventListenerImpl extends BaseEnversEventListener i
event.getPersister(), event.getPersister(),
entityName, entityName,
newDbState, newDbState,
event.getOldState(), oldState,
event.getSession() event.getSession()
); );
} }
} }
} }
private Object[] getOldDBState(AuditProcess auditProcess, String entityName, PostUpdateEvent event) {
if ( isDetachedEntityUpdate( entityName, event.getOldState() ) ) {
return auditProcess.getCachedEntityState( event.getId(), entityName );
}
return event.getOldState();
}
private Object[] postUpdateDBState(PostUpdateEvent event) { private Object[] postUpdateDBState(PostUpdateEvent event) {
final Object[] newDbState = event.getState().clone(); final Object[] newDbState = event.getState().clone();
if ( event.getOldState() != null ) { if ( event.getOldState() != null ) {

View File

@ -0,0 +1,40 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.envers.event.spi;
import org.hibernate.envers.boot.internal.EnversService;
import org.hibernate.envers.internal.synchronization.AuditProcess;
import org.hibernate.event.spi.PreUpdateEvent;
import org.hibernate.event.spi.PreUpdateEventListener;
/**
* Envers-specific entity (pre) update event listener.
*
* @author Chris Cranford
*/
public class EnversPreUpdateEventListenerImpl extends BaseEnversUpdateEventListener implements PreUpdateEventListener {
public EnversPreUpdateEventListenerImpl(EnversService enversService) {
super( enversService );
}
@Override
public boolean onPreUpdate(PreUpdateEvent event) {
final String entityName = event.getPersister().getEntityName();
if ( getEnversService().getEntitiesConfigurations().isVersioned( entityName ) ) {
checkIfTransactionInProgress( event.getSession() );
if ( isDetachedEntityUpdate( entityName, event.getOldState() ) ) {
final AuditProcess auditProcess = getEnversService().getAuditProcessManager().get( event.getSession() );
auditProcess.cacheEntityState(
event.getId(),
entityName,
event.getPersister().getDatabaseSnapshot( event.getId(), event.getSession() )
);
}
}
return false;
}
}

View File

@ -25,6 +25,7 @@ import org.hibernate.property.access.spi.Setter;
* @author Adam Warski (adam at warski dot org) * @author Adam Warski (adam at warski dot org)
* @author Michal Skowronek (mskowr at o2 dot pl) * @author Michal Skowronek (mskowr at o2 dot pl)
* @author Lukasz Zuchowski (author at zuchos dot com) * @author Lukasz Zuchowski (author at zuchos dot com)
* @author Chris Cranford
*/ */
public class ComponentPropertyMapper implements PropertyMapper, CompositeMapperBuilder { public class ComponentPropertyMapper implements PropertyMapper, CompositeMapperBuilder {
private final PropertyData propertyData; private final PropertyData propertyData;
@ -158,4 +159,9 @@ public class ComponentPropertyMapper implements PropertyMapper, CompositeMapperB
public Map<PropertyData, PropertyMapper> getProperties() { public Map<PropertyData, PropertyMapper> getProperties() {
return delegate.getProperties(); return delegate.getProperties();
} }
@Override
public boolean hasPropertiesWithModifiedFlag() {
return delegate.hasPropertiesWithModifiedFlag();
}
} }

View File

@ -0,0 +1,23 @@
/*
* Hibernate, Relational Persistence for Idiomatic Java
*
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
* See the lgpl.txt file in the root directory or <http://www.gnu.org/licenses/lgpl-2.1.html>.
*/
package org.hibernate.envers.internal.entities.mapper;
/**
* Contract for {@link PropertyMapper} implementations to expose whether they contain any property
* that uses {@link org.hibernate.envers.internal.entities.PropertyData#isUsingModifiedFlag()}.
*
* @author Chris Cranford
*/
public interface ModifiedFlagMapperSupport {
/**
* Returns whether the associated {@link PropertyMapper} has any properties that use
* the {@code witModifiedFlag} feature.
*
* @return {@code true} if a property uses {@code withModifiedFlag}, otherwise {@code false}.
*/
boolean hasPropertiesWithModifiedFlag();
}

View File

@ -233,4 +233,14 @@ public class MultiPropertyMapper implements ExtendedPropertyMapper {
public Map<String, PropertyData> getPropertyDatas() { public Map<String, PropertyData> getPropertyDatas() {
return propertyDatas; return propertyDatas;
} }
@Override
public boolean hasPropertiesWithModifiedFlag() {
for ( PropertyData property : getProperties().keySet() ) {
if ( property.isUsingModifiedFlag() ) {
return true;
}
}
return false;
}
} }

View File

@ -18,8 +18,9 @@ import org.hibernate.envers.internal.reader.AuditReaderImplementor;
/** /**
* @author Adam Warski (adam at warski dot org) * @author Adam Warski (adam at warski dot org)
* @author Michal Skowronek (mskowr at o2 dot pl) * @author Michal Skowronek (mskowr at o2 dot pl)
* @author Chris Cranford
*/ */
public interface PropertyMapper { public interface PropertyMapper extends ModifiedFlagMapperSupport {
/** /**
* Maps properties to the given map, basing on differences between properties of new and old objects. * Maps properties to the given map, basing on differences between properties of new and old objects.
* *

View File

@ -133,4 +133,8 @@ public class SinglePropertyMapper implements PropertyMapper, SimpleMapperBuilder
return null; return null;
} }
@Override
public boolean hasPropertiesWithModifiedFlag() {
return propertyData != null && propertyData.isUsingModifiedFlag();
}
} }

View File

@ -23,6 +23,7 @@ import org.hibernate.envers.internal.reader.AuditReaderImplementor;
* *
* @author Adam Warski (adam at warski dot org) * @author Adam Warski (adam at warski dot org)
* @author Michal Skowronek (mskowr at o2 dot pl) * @author Michal Skowronek (mskowr at o2 dot pl)
* @author Chris Cranford
*/ */
public class SubclassPropertyMapper implements ExtendedPropertyMapper { public class SubclassPropertyMapper implements ExtendedPropertyMapper {
private ExtendedPropertyMapper main; private ExtendedPropertyMapper main;
@ -140,4 +141,16 @@ public class SubclassPropertyMapper implements ExtendedPropertyMapper {
joinedProperties.putAll( main.getProperties() ); joinedProperties.putAll( main.getProperties() );
return joinedProperties; return joinedProperties;
} }
@Override
public boolean hasPropertiesWithModifiedFlag() {
// checks all properties, exposed both by the main mapper and parent mapper.
for ( PropertyData property : getProperties().keySet() ) {
if ( property.isUsingModifiedFlag() ) {
return true;
}
}
return false;
}
} }

View File

@ -38,6 +38,7 @@ import org.hibernate.property.access.spi.Setter;
/** /**
* @author Adam Warski (adam at warski dot org) * @author Adam Warski (adam at warski dot org)
* @author Michal Skowronek (mskowr at o2 dot pl) * @author Michal Skowronek (mskowr at o2 dot pl)
* @author Chris Cranford
*/ */
public abstract class AbstractCollectionMapper<T> implements PropertyMapper { public abstract class AbstractCollectionMapper<T> implements PropertyMapper {
protected final CommonCollectionMapperData commonCollectionMapperData; protected final CommonCollectionMapperData commonCollectionMapperData;
@ -395,4 +396,13 @@ public abstract class AbstractCollectionMapper<T> implements PropertyMapper {
return collectionChanges; return collectionChanges;
} }
@Override
public boolean hasPropertiesWithModifiedFlag() {
if ( commonCollectionMapperData != null ) {
final PropertyData propertyData = commonCollectionMapperData.getCollectionReferencingPropertyData();
return propertyData != null && propertyData.isUsingModifiedFlag();
}
return false;
}
} }

View File

@ -26,6 +26,7 @@ import org.hibernate.service.ServiceRegistry;
* Base class for property mappers that manage to-one relation. * Base class for property mappers that manage to-one relation.
* *
* @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com) * @author Lukasz Antoniak (lukasz dot antoniak at gmail dot com)
* @author Chris Cranford
*/ */
public abstract class AbstractToOneMapper implements PropertyMapper { public abstract class AbstractToOneMapper implements PropertyMapper {
private final ServiceRegistry serviceRegistry; private final ServiceRegistry serviceRegistry;
@ -135,4 +136,9 @@ public abstract class AbstractToOneMapper implements PropertyMapper {
return audited; return audited;
} }
} }
@Override
public boolean hasPropertiesWithModifiedFlag() {
return propertyData != null && propertyData.isUsingModifiedFlag();
}
} }

View File

@ -16,6 +16,7 @@ import javax.persistence.FlushModeType;
import org.hibernate.Session; import org.hibernate.Session;
import org.hibernate.action.spi.BeforeTransactionCompletionProcess; import org.hibernate.action.spi.BeforeTransactionCompletionProcess;
import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.envers.exception.AuditException;
import org.hibernate.envers.internal.revisioninfo.RevisionInfoGenerator; import org.hibernate.envers.internal.revisioninfo.RevisionInfoGenerator;
import org.hibernate.envers.internal.synchronization.work.AuditWorkUnit; import org.hibernate.envers.internal.synchronization.work.AuditWorkUnit;
import org.hibernate.envers.tools.Pair; import org.hibernate.envers.tools.Pair;
@ -24,6 +25,7 @@ import org.jboss.logging.Logger;
/** /**
* @author Adam Warski (adam at warski dot org) * @author Adam Warski (adam at warski dot org)
* @author Chris Cranford
*/ */
public class AuditProcess implements BeforeTransactionCompletionProcess { public class AuditProcess implements BeforeTransactionCompletionProcess {
private static final Logger log = Logger.getLogger( AuditProcess.class ); private static final Logger log = Logger.getLogger( AuditProcess.class );
@ -34,8 +36,8 @@ public class AuditProcess implements BeforeTransactionCompletionProcess {
private final LinkedList<AuditWorkUnit> workUnits; private final LinkedList<AuditWorkUnit> workUnits;
private final Queue<AuditWorkUnit> undoQueue; private final Queue<AuditWorkUnit> undoQueue;
private final Map<Pair<String, Object>, AuditWorkUnit> usedIds; private final Map<Pair<String, Object>, AuditWorkUnit> usedIds;
private final Map<Pair<String, Object>, Object[]> entityStateCache;
private final EntityChangeNotifier entityChangeNotifier; private final EntityChangeNotifier entityChangeNotifier;
private Object revisionData; private Object revisionData;
public AuditProcess(RevisionInfoGenerator revisionInfoGenerator, SessionImplementor session) { public AuditProcess(RevisionInfoGenerator revisionInfoGenerator, SessionImplementor session) {
@ -45,9 +47,27 @@ public class AuditProcess implements BeforeTransactionCompletionProcess {
workUnits = new LinkedList<>(); workUnits = new LinkedList<>();
undoQueue = new LinkedList<>(); undoQueue = new LinkedList<>();
usedIds = new HashMap<>(); usedIds = new HashMap<>();
entityStateCache = new HashMap<>();
entityChangeNotifier = new EntityChangeNotifier( revisionInfoGenerator, session ); entityChangeNotifier = new EntityChangeNotifier( revisionInfoGenerator, session );
} }
public void cacheEntityState(Object id, String entityName, Object[] snapshot) {
final Pair<String, Object> key = new Pair<>( entityName, id );
if ( entityStateCache.containsKey( key ) ) {
throw new AuditException( "The entity [" + entityName + "] with id [" + id + "] is already cached." );
}
entityStateCache.put( key, snapshot );
}
public Object[] getCachedEntityState(Object id, String entityName) {
final Pair<String, Object> key = new Pair<>( entityName, id );
final Object[] entityState = entityStateCache.get( key );
if ( entityState != null ) {
entityStateCache.remove( key );
}
return entityState;
}
private void removeWorkUnit(AuditWorkUnit vwu) { private void removeWorkUnit(AuditWorkUnit vwu) {
workUnits.remove( vwu ); workUnits.remove( vwu );
if ( vwu.isPerformed() ) { if ( vwu.isPerformed() ) {