Skip to content

Commit

Permalink
HHH-8973 - Fix tracking entity modifications to detached entities wit…
Browse files Browse the repository at this point in the history
…h the modified flags feature.
  • Loading branch information
Naros committed Jan 18, 2017
1 parent ad0cd4f commit ebf4803
Show file tree
Hide file tree
Showing 13 changed files with 189 additions and 5 deletions.
Expand Up @@ -18,6 +18,7 @@
import org.hibernate.envers.event.spi.EnversPostUpdateEventListenerImpl;
import org.hibernate.envers.event.spi.EnversPreCollectionRemoveEventListenerImpl;
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.spi.EventType;
import org.hibernate.integrator.spi.Integrator;
Expand All @@ -29,6 +30,7 @@
* Hooks up Envers event listeners.
*
* @author Steve Ebersole
* @author Chris Cranford
*/
public class EnversIntegrator implements Integrator {
private static final Logger log = Logger.getLogger( EnversIntegrator.class );
Expand Down Expand Up @@ -89,6 +91,10 @@ public void integrate(
EventType.POST_INSERT,
new EnversPostInsertEventListenerImpl( enversService )
);
listenerRegistry.appendListeners(
EventType.PRE_UPDATE,
new EnversPreUpdateEventListenerImpl( enversService )
);
listenerRegistry.appendListeners(
EventType.POST_UPDATE,
new EnversPostUpdateEventListenerImpl( enversService )
Expand Down
@@ -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;
}
}
Expand Up @@ -20,8 +20,9 @@
* @author Adam Warski (adam at warski dot org)
* @author HernпїЅn Chanfreau
* @author Steve Ebersole
* @author Chris Cranford
*/
public class EnversPostUpdateEventListenerImpl extends BaseEnversEventListener implements PostUpdateEventListener {
public class EnversPostUpdateEventListenerImpl extends BaseEnversUpdateEventListener implements PostUpdateEventListener {
public EnversPostUpdateEventListenerImpl(EnversService enversService) {
super( enversService );
}
Expand All @@ -34,6 +35,8 @@ public void onPostUpdate(PostUpdateEvent event) {
checkIfTransactionInProgress( event.getSession() );

final AuditProcess auditProcess = getEnversService().getAuditProcessManager().get( event.getSession() );

Object[] oldState = getOldDBState( auditProcess, entityName, event );
final Object[] newDbState = postUpdateDBState( event );
final AuditWorkUnit workUnit = new ModWorkUnit(
event.getSession(),
Expand All @@ -42,7 +45,7 @@ public void onPostUpdate(PostUpdateEvent event) {
event.getId(),
event.getPersister(),
newDbState,
event.getOldState()
oldState
);
auditProcess.addWorkUnit( workUnit );

Expand All @@ -52,13 +55,20 @@ public void onPostUpdate(PostUpdateEvent event) {
event.getPersister(),
entityName,
newDbState,
event.getOldState(),
oldState,
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) {
final Object[] newDbState = event.getState().clone();
if ( event.getOldState() != null ) {
Expand Down
@@ -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;
}
}
Expand Up @@ -25,6 +25,7 @@
* @author Adam Warski (adam at warski dot org)
* @author Michal Skowronek (mskowr at o2 dot pl)
* @author Lukasz Zuchowski (author at zuchos dot com)
* @author Chris Cranford
*/
public class ComponentPropertyMapper implements PropertyMapper, CompositeMapperBuilder {
private final PropertyData propertyData;
Expand Down Expand Up @@ -158,4 +159,9 @@ public List<PersistentCollectionChangeData> mapCollectionChanges(
public Map<PropertyData, PropertyMapper> getProperties() {
return delegate.getProperties();
}

@Override
public boolean hasPropertiesWithModifiedFlag() {
return delegate.hasPropertiesWithModifiedFlag();
}
}
@@ -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();
}
Expand Up @@ -233,4 +233,14 @@ public Map<PropertyData, PropertyMapper> getProperties() {
public Map<String, PropertyData> getPropertyDatas() {
return propertyDatas;
}

@Override
public boolean hasPropertiesWithModifiedFlag() {
for ( PropertyData property : getProperties().keySet() ) {
if ( property.isUsingModifiedFlag() ) {
return true;
}
}
return false;
}
}
Expand Up @@ -18,8 +18,9 @@
/**
* @author Adam Warski (adam at warski dot org)
* @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.
*
Expand Down
Expand Up @@ -133,4 +133,8 @@ public List<PersistentCollectionChangeData> mapCollectionChanges(
return null;
}

@Override
public boolean hasPropertiesWithModifiedFlag() {
return propertyData != null && propertyData.isUsingModifiedFlag();
}
}
Expand Up @@ -23,6 +23,7 @@
*
* @author Adam Warski (adam at warski dot org)
* @author Michal Skowronek (mskowr at o2 dot pl)
* @author Chris Cranford
*/
public class SubclassPropertyMapper implements ExtendedPropertyMapper {
private ExtendedPropertyMapper main;
Expand Down Expand Up @@ -140,4 +141,16 @@ public Map<PropertyData, PropertyMapper> getProperties() {
joinedProperties.putAll( main.getProperties() );
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;
}

}
Expand Up @@ -38,6 +38,7 @@
/**
* @author Adam Warski (adam at warski dot org)
* @author Michal Skowronek (mskowr at o2 dot pl)
* @author Chris Cranford
*/
public abstract class AbstractCollectionMapper<T> implements PropertyMapper {
protected final CommonCollectionMapperData commonCollectionMapperData;
Expand Down Expand Up @@ -395,4 +396,13 @@ private List<PersistentCollectionChangeData> mapCollectionChanges(

return collectionChanges;
}

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

@Override
public boolean hasPropertiesWithModifiedFlag() {
return propertyData != null && propertyData.isUsingModifiedFlag();
}
}
Expand Up @@ -16,6 +16,7 @@
import org.hibernate.Session;
import org.hibernate.action.spi.BeforeTransactionCompletionProcess;
import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.envers.exception.AuditException;
import org.hibernate.envers.internal.revisioninfo.RevisionInfoGenerator;
import org.hibernate.envers.internal.synchronization.work.AuditWorkUnit;
import org.hibernate.envers.tools.Pair;
Expand All @@ -24,6 +25,7 @@

/**
* @author Adam Warski (adam at warski dot org)
* @author Chris Cranford
*/
public class AuditProcess implements BeforeTransactionCompletionProcess {
private static final Logger log = Logger.getLogger( AuditProcess.class );
Expand All @@ -34,8 +36,8 @@ public class AuditProcess implements BeforeTransactionCompletionProcess {
private final LinkedList<AuditWorkUnit> workUnits;
private final Queue<AuditWorkUnit> undoQueue;
private final Map<Pair<String, Object>, AuditWorkUnit> usedIds;
private final Map<Pair<String, Object>, Object[]> entityStateCache;
private final EntityChangeNotifier entityChangeNotifier;

private Object revisionData;

public AuditProcess(RevisionInfoGenerator revisionInfoGenerator, SessionImplementor session) {
Expand All @@ -45,9 +47,27 @@ public AuditProcess(RevisionInfoGenerator revisionInfoGenerator, SessionImplemen
workUnits = new LinkedList<>();
undoQueue = new LinkedList<>();
usedIds = new HashMap<>();
entityStateCache = new HashMap<>();
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) {
workUnits.remove( vwu );
if ( vwu.isPerformed() ) {
Expand Down

0 comments on commit ebf4803

Please sign in to comment.