HHH-5845 (partial fix) Lazy Loading of audited entites with revision type 'delete'

Fixed for to-one associations, but not for one-to-many and other collection types.
This commit is contained in:
Adar Dembo 2013-03-26 16:37:00 -07:00 committed by Lukasz Antoniak
parent 378fc1378a
commit 52ab4baafb
9 changed files with 170 additions and 20 deletions

View File

@ -71,7 +71,26 @@ public interface AuditReader {
<T> T find(Class<T> cls, String entityName, Object primaryKey, <T> T find(Class<T> cls, String entityName, Object primaryKey,
Number revision) throws IllegalArgumentException, Number revision) throws IllegalArgumentException,
NotAuditedException, IllegalStateException; NotAuditedException, IllegalStateException;
/**
* Find an entity by primary key at the given revision with the specified entityName,
* possibly including deleted entities in the search.
* @param cls Class of the entity.
* @param entityName Name of the entity (if can't be guessed basing on the {@code cls}).
* @param primaryKey Primary key of the entity.
* @param revision Revision in which to get the entity.
* @param includeDeletions Whether to include deleted entities in the search.
* @return The found entity instance at the given revision (its properties may be partially filled
* if not all properties are audited) or null, if an entity with that id didn't exist at that
* revision.
* @throws IllegalArgumentException If cls or primaryKey is null or revision is less or equal to 0.
* @throws NotAuditedException When entities of the given class are not audited.
* @throws IllegalStateException If the associated entity manager is closed.
*/
<T> T find(Class<T> cls, String entityName, Object primaryKey,
Number revision, boolean includeDeletions) throws IllegalArgumentException,
NotAuditedException, IllegalStateException;
/** /**
* Get a list of revision numbers, at which an entity was modified. * Get a list of revision numbers, at which an entity was modified.
* @param cls Class of the entity. * @param cls Class of the entity.

View File

@ -23,6 +23,7 @@
*/ */
package org.hibernate.envers.entities; package org.hibernate.envers.entities;
import org.hibernate.envers.RevisionType;
import org.hibernate.envers.configuration.AuditConfiguration; import org.hibernate.envers.configuration.AuditConfiguration;
import org.hibernate.envers.entities.mapper.id.IdMapper; import org.hibernate.envers.entities.mapper.id.IdMapper;
import org.hibernate.envers.entities.mapper.relation.lazy.ToOneDelegateSessionImplementor; import org.hibernate.envers.entities.mapper.relation.lazy.ToOneDelegateSessionImplementor;
@ -77,7 +78,7 @@ public class EntityInstantiator {
// Fixes HHH-4751 issue (@IdClass with @ManyToOne relation mapping inside) // Fixes HHH-4751 issue (@IdClass with @ManyToOne relation mapping inside)
// Note that identifiers are always audited // Note that identifiers are always audited
// Replace identifier proxies if do not point to audit tables // Replace identifier proxies if do not point to audit tables
replaceNonAuditIdProxies(originalId, revision); replaceNonAuditIdProxies(versionsEntity, revision);
Object primaryKey = idMapper.mapToIdFromMap(originalId); Object primaryKey = idMapper.mapToIdFromMap(originalId);
@ -116,7 +117,8 @@ public class EntityInstantiator {
} }
@SuppressWarnings({"unchecked"}) @SuppressWarnings({"unchecked"})
private void replaceNonAuditIdProxies(Map originalId, Number revision) { private void replaceNonAuditIdProxies(Map versionsEntity, Number revision) {
final Map originalId = (Map) versionsEntity.get( verCfg.getAuditEntCfg().getOriginalIdPropName() );
for (Object key : originalId.keySet()) { for (Object key : originalId.keySet()) {
Object value = originalId.get(key); Object value = originalId.get(key);
if (value instanceof HibernateProxy) { if (value instanceof HibernateProxy) {
@ -133,7 +135,10 @@ public class EntityInstantiator {
catch ( ClassNotFoundException e ) { catch ( ClassNotFoundException e ) {
throw new AuditException( e ); throw new AuditException( e );
} }
final ToOneDelegateSessionImplementor delegate = new ToOneDelegateSessionImplementor(versionsReader, entityClass, entityId, revision, verCfg); final ToOneDelegateSessionImplementor delegate = new ToOneDelegateSessionImplementor(
versionsReader, entityClass, entityId, revision,
RevisionType.DEL.equals( versionsEntity.get( verCfg.getAuditEntCfg().getRevisionTypePropName() ) ),
verCfg);
originalId.put(key, originalId.put(key,
versionsReader.getSessionImplementor().getFactory().getEntityPersister(entityName).createProxy(entityId, delegate)); versionsReader.getSessionImplementor().getFactory().getEntityPersister(entityName).createProxy(entityId, delegate));
} }

View File

@ -15,10 +15,12 @@ public class ToOneEntityLoader {
* Immediately loads historical entity or its current state when excluded from audit process. * Immediately loads historical entity or its current state when excluded from audit process.
*/ */
public static Object loadImmediate(AuditReaderImplementor versionsReader, Class<?> entityClass, String entityName, public static Object loadImmediate(AuditReaderImplementor versionsReader, Class<?> entityClass, String entityName,
Object entityId, Number revision, AuditConfiguration verCfg) { Object entityId, Number revision, boolean removed, AuditConfiguration verCfg) {
if ( verCfg.getEntCfg().getNotVersionEntityConfiguration( entityName ) == null ) { if ( verCfg.getEntCfg().getNotVersionEntityConfiguration( entityName ) == null ) {
// Audited relation, look up entity with Envers. // Audited relation, look up entity with Envers.
return versionsReader.find( entityClass, entityName, entityId, revision ); // When user traverses removed entities graph, do not restrict revision type of referencing objects
// to ADD or MOD (DEL possible). See HHH-5845.
return versionsReader.find( entityClass, entityName, entityId, revision, removed);
} }
else { else {
// Not audited relation, look up entity with Hibernate. // Not audited relation, look up entity with Hibernate.
@ -30,11 +32,11 @@ public class ToOneEntityLoader {
* Creates proxy of referenced *-to-one entity. * Creates proxy of referenced *-to-one entity.
*/ */
public static Object createProxy(AuditReaderImplementor versionsReader, Class<?> entityClass, String entityName, public static Object createProxy(AuditReaderImplementor versionsReader, Class<?> entityClass, String entityName,
Object entityId, Number revision, AuditConfiguration verCfg) { Object entityId, Number revision, boolean removed, AuditConfiguration verCfg) {
EntityPersister persister = versionsReader.getSessionImplementor().getFactory().getEntityPersister( entityName ); EntityPersister persister = versionsReader.getSessionImplementor().getFactory().getEntityPersister( entityName );
return persister.createProxy( return persister.createProxy(
(Serializable) entityId, (Serializable) entityId,
new ToOneDelegateSessionImplementor( versionsReader, entityClass, entityId, revision, verCfg ) new ToOneDelegateSessionImplementor( versionsReader, entityClass, entityId, revision, removed, verCfg )
); );
} }
@ -43,11 +45,11 @@ public class ToOneEntityLoader {
* allowed (e.g. @Proxy(lazy=false), final class). * allowed (e.g. @Proxy(lazy=false), final class).
*/ */
public static Object createProxyOrLoadImmediate(AuditReaderImplementor versionsReader, Class<?> entityClass, String entityName, public static Object createProxyOrLoadImmediate(AuditReaderImplementor versionsReader, Class<?> entityClass, String entityName,
Object entityId, Number revision, AuditConfiguration verCfg) { Object entityId, Number revision, boolean removed, AuditConfiguration verCfg) {
EntityPersister persister = versionsReader.getSessionImplementor().getFactory().getEntityPersister( entityName ); EntityPersister persister = versionsReader.getSessionImplementor().getFactory().getEntityPersister( entityName );
if ( persister.hasProxy() ) { if ( persister.hasProxy() ) {
return createProxy( versionsReader, entityClass, entityName, entityId, revision, verCfg ); return createProxy( versionsReader, entityClass, entityName, entityId, revision, removed, verCfg );
} }
return loadImmediate( versionsReader, entityClass, entityName, entityId, revision, verCfg ); return loadImmediate( versionsReader, entityClass, entityName, entityId, revision, removed, verCfg );
} }
} }

View File

@ -28,6 +28,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.SessionImplementor;
import org.hibernate.envers.RevisionType;
import org.hibernate.envers.configuration.AuditConfiguration; import org.hibernate.envers.configuration.AuditConfiguration;
import org.hibernate.envers.entities.PropertyData; import org.hibernate.envers.entities.PropertyData;
import org.hibernate.envers.entities.mapper.id.IdMapper; import org.hibernate.envers.entities.mapper.id.IdMapper;
@ -99,7 +100,7 @@ public class ToOneIdMapper extends AbstractToOneMapper {
EntityInfo referencedEntity = getEntityInfo(verCfg, referencedEntityName); EntityInfo referencedEntity = getEntityInfo(verCfg, referencedEntityName);
value = ToOneEntityLoader.createProxyOrLoadImmediate( value = ToOneEntityLoader.createProxyOrLoadImmediate(
versionsReader, referencedEntity.getEntityClass(), referencedEntityName, versionsReader, referencedEntity.getEntityClass(), referencedEntityName,
entityId, revision, verCfg entityId, revision, RevisionType.DEL.equals( data.get( verCfg.getAuditEntCfg().getRevisionTypePropName() ) ), verCfg
); );
} }
} }

View File

@ -42,20 +42,22 @@ public class ToOneDelegateSessionImplementor extends AbstractDelegateSessionImpl
private final Class<?> entityClass; private final Class<?> entityClass;
private final Object entityId; private final Object entityId;
private final Number revision; private final Number revision;
private final boolean removed;
private final AuditConfiguration verCfg; private final AuditConfiguration verCfg;
public ToOneDelegateSessionImplementor(AuditReaderImplementor versionsReader, public ToOneDelegateSessionImplementor(AuditReaderImplementor versionsReader,
Class<?> entityClass, Object entityId, Number revision, Class<?> entityClass, Object entityId, Number revision, boolean removed,
AuditConfiguration verCfg) { AuditConfiguration verCfg) {
super(versionsReader.getSessionImplementor()); super(versionsReader.getSessionImplementor());
this.versionsReader = versionsReader; this.versionsReader = versionsReader;
this.entityClass = entityClass; this.entityClass = entityClass;
this.entityId = entityId; this.entityId = entityId;
this.revision = revision; this.revision = revision;
this.removed = removed;
this.verCfg = verCfg; this.verCfg = verCfg;
} }
public Object doImmediateLoad(String entityName) throws HibernateException { public Object doImmediateLoad(String entityName) throws HibernateException {
return ToOneEntityLoader.loadImmediate( versionsReader, entityClass, entityName, entityId, revision, verCfg ); return ToOneEntityLoader.loadImmediate( versionsReader, entityClass, entityName, entityId, revision, removed, verCfg );
} }
} }

View File

@ -80,6 +80,25 @@ public class AuditQueryCreator {
return new EntitiesAtRevisionQuery(auditCfg, auditReaderImplementor, c, entityName, revision); return new EntitiesAtRevisionQuery(auditCfg, auditReaderImplementor, c, entityName, revision);
} }
/**
* Creates a query, which will return entities satisfying some conditions (specified later),
* at a given revision and a given entityName. Deleted entities may be optionally
* included.
* @param c Class of the entities for which to query.
* @param entityName Name of the entity (if can't be guessed basing on the {@code c}).
* @param revision Revision number at which to execute the query.
* @param includeDeletions Whether to include deleted entities in the search.
* @return A query for entities at a given revision, to which conditions can be added and which
* can then be executed. The result of the query will be a list of entities (beans), unless a
* projection is added.
*/
public AuditQuery forEntitiesAtRevision(Class<?> c, String entityName, Number revision, boolean includeDeletions) {
checkNotNull(revision, "Entity revision");
checkPositive(revision, "Entity revision");
c = getTargetClassIfProxied(c);
return new EntitiesAtRevisionQuery(auditCfg, auditReaderImplementor, c, entityName, revision, includeDeletions);
}
/** /**
* Creates a query, which will return entities modified at the specified revision. * Creates a query, which will return entities modified at the specified revision.
* *

View File

@ -45,20 +45,31 @@ import static org.hibernate.envers.entities.mapper.relation.query.QueryConstants
*/ */
public class EntitiesAtRevisionQuery extends AbstractAuditQuery { public class EntitiesAtRevisionQuery extends AbstractAuditQuery {
private final Number revision; private final Number revision;
private final boolean includeDeletions;
public EntitiesAtRevisionQuery(AuditConfiguration verCfg, public EntitiesAtRevisionQuery(AuditConfiguration verCfg,
AuditReaderImplementor versionsReader, Class<?> cls, AuditReaderImplementor versionsReader, Class<?> cls,
Number revision) { Number revision) {
super(verCfg, versionsReader, cls); super(verCfg, versionsReader, cls);
this.revision = revision; this.revision = revision;
this.includeDeletions = false;
} }
public EntitiesAtRevisionQuery(AuditConfiguration verCfg, public EntitiesAtRevisionQuery(AuditConfiguration verCfg,
AuditReaderImplementor versionsReader, Class<?> cls, String entityName, Number revision) { AuditReaderImplementor versionsReader, Class<?> cls, String entityName, Number revision) {
super(verCfg, versionsReader, cls, entityName); super(verCfg, versionsReader, cls, entityName);
this.revision = revision; this.revision = revision;
} this.includeDeletions = false;
}
public EntitiesAtRevisionQuery(AuditConfiguration verCfg,
AuditReaderImplementor versionsReader, Class<?> cls,
String entityName, Number revision, boolean includeDeletions) {
super(verCfg, versionsReader, cls, entityName);
this.revision = revision;
this.includeDeletions = includeDeletions;
}
@SuppressWarnings({"unchecked"}) @SuppressWarnings({"unchecked"})
public List list() { public List list() {
/* /*
@ -91,8 +102,10 @@ public class EntitiesAtRevisionQuery extends AbstractAuditQuery {
verEntCfg.getRevisionEndFieldName(), true, referencedIdData, verEntCfg.getRevisionEndFieldName(), true, referencedIdData,
revisionPropertyPath, originalIdPropertyName, REFERENCED_ENTITY_ALIAS, REFERENCED_ENTITY_ALIAS_DEF_AUD_STR); revisionPropertyPath, originalIdPropertyName, REFERENCED_ENTITY_ALIAS, REFERENCED_ENTITY_ALIAS_DEF_AUD_STR);
// e.revision_type != DEL // e.revision_type != DEL
qb.getRootParameters().addWhereWithParam(verEntCfg.getRevisionTypePropName(), "<>", RevisionType.DEL); if (!includeDeletions) {
qb.getRootParameters().addWhereWithParam(verEntCfg.getRevisionTypePropName(), "<>", RevisionType.DEL);
}
// all specified conditions // all specified conditions
for (AuditCriterion criterion : criterions) { for (AuditCriterion criterion : criterions) {

View File

@ -95,9 +95,15 @@ public class AuditReaderImpl implements AuditReaderImplementor {
cls = getTargetClassIfProxied(cls); cls = getTargetClassIfProxied(cls);
return this.find(cls, cls.getName(), primaryKey, revision); return this.find(cls, cls.getName(), primaryKey, revision);
} }
public <T> T find(Class<T> cls, String entityName, Object primaryKey, Number revision)
throws IllegalArgumentException, NotAuditedException, IllegalStateException {
return this.find(cls, entityName, primaryKey, revision, false);
}
@SuppressWarnings({"unchecked"}) @SuppressWarnings({"unchecked"})
public <T> T find(Class<T> cls, String entityName, Object primaryKey, Number revision) throws public <T> T find(Class<T> cls, String entityName, Object primaryKey, Number revision,
boolean includeDeletions) throws
IllegalArgumentException, NotAuditedException, IllegalStateException { IllegalArgumentException, NotAuditedException, IllegalStateException {
cls = getTargetClassIfProxied(cls); cls = getTargetClassIfProxied(cls);
checkNotNull(cls, "Entity class"); checkNotNull(cls, "Entity class");
@ -118,7 +124,7 @@ public class AuditReaderImpl implements AuditReaderImplementor {
Object result; Object result;
try { try {
// The result is put into the cache by the entity instantiator called from the query // The result is put into the cache by the entity instantiator called from the query
result = createQuery().forEntitiesAtRevision(cls, entityName, revision) result = createQuery().forEntitiesAtRevision(cls, entityName, revision, includeDeletions)
.add(AuditEntity.id().eq(primaryKey)).getSingleResult(); .add(AuditEntity.id().eq(primaryKey)).getSingleResult();
} catch (NoResultException e) { } catch (NoResultException e) {
result = null; result = null;

View File

@ -0,0 +1,83 @@
package org.hibernate.envers.test.integration.proxy;
import org.hibernate.Hibernate;
import org.hibernate.envers.RevisionType;
import org.hibernate.envers.query.AuditEntity;
import org.hibernate.envers.query.AuditQuery;
import org.hibernate.envers.test.BaseEnversJPAFunctionalTestCase;
import org.hibernate.envers.test.Priority;
import org.hibernate.envers.test.entities.onetomany.SetRefEdEntity;
import org.hibernate.envers.test.entities.onetomany.SetRefIngEntity;
import org.hibernate.envers.test.tools.TestTools;
import org.hibernate.testing.FailureExpected;
import org.hibernate.testing.TestForIssue;
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
import java.util.Map;
import javax.persistence.EntityManager;
@TestForIssue(jiraKey = "HHH-5845")
public class RemovedObjectQueryTest extends BaseEnversJPAFunctionalTestCase {
@Override
@SuppressWarnings("unchecked")
protected void addConfigOptions(Map options) {
options.put("org.hibernate.envers.store_data_at_delete", "true");
}
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] { SetRefEdEntity.class, SetRefIngEntity.class };
}
@Test
@Priority(10)
public void initData() {
EntityManager em = getEntityManager();
SetRefEdEntity refEdEntity = new SetRefEdEntity(1, "Demo Data");
SetRefIngEntity refIngEntity = new SetRefIngEntity(2, "Example Data", refEdEntity);
em.getTransaction().begin();
em.persist(refEdEntity);
em.persist(refIngEntity);
em.getTransaction().commit();
em.getTransaction().begin();
refIngEntity = em.find(SetRefIngEntity.class, 2);
em.remove(refIngEntity);
em.remove(refEdEntity);
em.getTransaction().commit();
}
@Test
public void testFindDeletedReference() {
AuditQuery query = getAuditReader().createQuery().forRevisionsOfEntity(SetRefIngEntity.class, false, true)
.add(AuditEntity.revisionType().eq(RevisionType.DEL));
List queryResult = (List) query.getResultList();
Object[] objArray = (Object[]) queryResult.get(0);
SetRefIngEntity refIngEntity = (SetRefIngEntity) objArray[0];
Assert.assertEquals("Example Data", refIngEntity.getData());
Hibernate.initialize(refIngEntity.getReference());
Assert.assertEquals("Demo Data", refIngEntity.getReference().getData());
}
@FailureExpected(jiraKey = "HHH-5845") // TODO: doesn't work until collection queries are fixed
@Test
public void testFindDeletedReferring() {
AuditQuery query = getAuditReader().createQuery().forRevisionsOfEntity(SetRefEdEntity.class, false, true)
.add(AuditEntity.revisionType().eq(RevisionType.DEL));
List queryResult = (List) query.getResultList();
Object[] objArray = (Object[]) queryResult.get(0);
SetRefEdEntity refEdEntity = (SetRefEdEntity) objArray[0];
Assert.assertEquals("Demo Data", refEdEntity.getData());
Hibernate.initialize(refEdEntity.getReffering());
Assert.assertEquals(TestTools.makeSet(new SetRefIngEntity(2, "Example Data")), refEdEntity.getReffering());
}
}