From 9ace8a9dd3d97858c10355be924730db7930c9d0 Mon Sep 17 00:00:00 2001 From: Chris Cranford Date: Fri, 17 Dec 2021 14:38:15 -0500 Subject: [PATCH] HHH-13817 Support to-one relation traversals in RevisionsOfEntity queries --- ...ava => AbstractAuditAssociationQuery.java} | 143 ++++++------- .../internal/impl/AbstractAuditQuery.java | 32 +-- .../EntitiesAtRevisionAssociationQuery.java | 120 +++++++++++ .../impl/EntitiesAtRevisionQuery.java | 34 +++- .../impl/EntitiesModifiedAtRevisionQuery.java | 35 +++- .../RevisionsOfEntityAssociationQuery.java | 76 +++++++ .../internal/impl/RevisionsOfEntityQuery.java | 48 ++++- .../envers/BaseEnversFunctionalTestCase.java | 10 + ...onsOfEntitiesQueryStoreAtDeletionTest.java | 25 +++ ...sociationRevisionsOfEntitiesQueryTest.java | 192 ++++++++++++++++++ 10 files changed, 596 insertions(+), 119 deletions(-) rename hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/{AuditAssociationQueryImpl.java => AbstractAuditAssociationQuery.java} (82%) create mode 100644 hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/EntitiesAtRevisionAssociationQuery.java create mode 100644 hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/RevisionsOfEntityAssociationQuery.java create mode 100644 hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/query/AssociationRevisionsOfEntitiesQueryStoreAtDeletionTest.java create mode 100644 hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/query/AssociationRevisionsOfEntitiesQueryTest.java diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/AuditAssociationQueryImpl.java b/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/AbstractAuditAssociationQuery.java similarity index 82% rename from hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/AuditAssociationQueryImpl.java rename to hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/AbstractAuditAssociationQuery.java index 9c8f6dfc4d..1a052524c0 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/AuditAssociationQueryImpl.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/AbstractAuditAssociationQuery.java @@ -7,6 +7,7 @@ package org.hibernate.envers.query.internal.impl; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; @@ -38,32 +39,37 @@ import org.hibernate.envers.query.order.AuditOrder; import org.hibernate.envers.query.projection.AuditProjection; /** + * An abstract base class for all {@link AuditAssociationQuery} implementations. + * * @author Felix Feisst (feisst dot felix at gmail dot com) * @author Chris Cranford */ @Incubating -public class AuditAssociationQueryImpl +public abstract class AbstractAuditAssociationQuery implements AuditAssociationQuery, AuditQueryImplementor { - private final EnversService enversService; - private final AuditReaderImplementor auditReader; - private final Q parent; - private final QueryBuilder queryBuilder; - private final JoinType joinType; - private final String entityName; - private final RelationDescription relationDescription; - private final ComponentDescription componentDescription; - private final String ownerAlias; - private final String ownerEntityName; - private final String alias; - private final Map aliasToEntityNameMap; - private final Map aliasToComponentPropertyNameMap; - private final List criterions = new ArrayList<>(); - private final AuditCriterion onClauseCriterion; - private final Parameters parameters; - private final List> associationQueries = new ArrayList<>(); + protected final EnversService enversService; + protected final AuditReaderImplementor auditReader; + protected final Q parent; + protected final QueryBuilder queryBuilder; + protected final JoinType joinType; + protected final String entityName; + protected final RelationDescription relationDescription; + protected final ComponentDescription componentDescription; + protected final String ownerAlias; + protected final String ownerEntityName; + protected final String alias; + protected final Map aliasToEntityNameMap; + protected final Map aliasToComponentPropertyNameMap; + protected final List criterions = new ArrayList<>(); + protected final AuditCriterion onClauseCriterion; + protected final Parameters parameters; - public AuditAssociationQueryImpl( + // todo: can these association query collections be merged? + protected final List> associationQueries = new ArrayList<>(); + protected final Map>> associationQueryMap = new HashMap<>(); + + public AbstractAuditAssociationQuery( final EnversService enversService, final AuditReaderImplementor auditReader, final Q parent, @@ -142,7 +148,7 @@ public class AuditAssociationQueryImpl } @Override - public AuditAssociationQueryImpl> traverseRelation( + public AbstractAuditAssociationQuery> traverseRelation( String associationName, JoinType joinType) { return traverseRelation( @@ -153,7 +159,7 @@ public class AuditAssociationQueryImpl } @Override - public AuditAssociationQueryImpl> traverseRelation( + public AbstractAuditAssociationQuery> traverseRelation( String associationName, JoinType joinType, String alias) { @@ -165,37 +171,34 @@ public class AuditAssociationQueryImpl ); } - @Override - public AuditAssociationQueryImpl> traverseRelation( + public AbstractAuditAssociationQuery> traverseRelation( String associationName, JoinType joinType, String alias, AuditCriterion onClause) { - AuditAssociationQueryImpl> result = new AuditAssociationQueryImpl<>( - enversService, - auditReader, - this, - queryBuilder, - associationName, - joinType, - aliasToEntityNameMap, - aliasToComponentPropertyNameMap, - this.alias, - alias, - onClause - ); - associationQueries.add( result ); - return result; + AbstractAuditAssociationQuery> query = associationQueryMap.get( associationName ); + if ( query == null ) { + query = createAssociationQuery( associationName, joinType, alias, onClause ); + associationQueries.add( (AbstractAuditAssociationQuery) query ); + associationQueryMap.put( associationName, query ); + } + return query; } + protected abstract AbstractAuditAssociationQuery> createAssociationQuery( + String associationName, + JoinType joinType, + String alias, + AuditCriterion onClause); + @Override - public AuditAssociationQueryImpl add(AuditCriterion criterion) { + public AbstractAuditAssociationQuery add(AuditCriterion criterion) { criterions.add( criterion ); return this; } @Override - public AuditAssociationQueryImpl addProjection(AuditProjection projection) { + public AbstractAuditAssociationQuery addProjection(AuditProjection projection) { String projectionEntityAlias = projection.getAlias( alias ); String projectionEntityName = aliasToEntityNameMap.get( projectionEntityAlias ); registerProjection( projectionEntityName, projection ); @@ -211,7 +214,7 @@ public class AuditAssociationQueryImpl } @Override - public AuditAssociationQueryImpl addOrder(AuditOrder order) { + public AbstractAuditAssociationQuery addOrder(AuditOrder order) { AuditOrder.OrderData orderData = order.getData( enversService.getConfig() ); String orderEntityAlias = orderData.getAlias( alias ); String orderEntityName = aliasToEntityNameMap.get( orderEntityAlias ); @@ -237,55 +240,55 @@ public class AuditAssociationQueryImpl } @Override - public AuditAssociationQueryImpl setMaxResults(int maxResults) { + public AbstractAuditAssociationQuery setMaxResults(int maxResults) { parent.setMaxResults( maxResults ); return this; } @Override - public AuditAssociationQueryImpl setFirstResult(int firstResult) { + public AbstractAuditAssociationQuery setFirstResult(int firstResult) { parent.setFirstResult( firstResult ); return this; } @Override - public AuditAssociationQueryImpl setCacheable(boolean cacheable) { + public AbstractAuditAssociationQuery setCacheable(boolean cacheable) { parent.setCacheable( cacheable ); return this; } @Override - public AuditAssociationQueryImpl setCacheRegion(String cacheRegion) { + public AbstractAuditAssociationQuery setCacheRegion(String cacheRegion) { parent.setCacheRegion( cacheRegion ); return this; } @Override - public AuditAssociationQueryImpl setComment(String comment) { + public AbstractAuditAssociationQuery setComment(String comment) { parent.setComment( comment ); return this; } @Override - public AuditAssociationQueryImpl setFlushMode(FlushMode flushMode) { + public AbstractAuditAssociationQuery setFlushMode(FlushMode flushMode) { parent.setFlushMode( flushMode ); return this; } @Override - public AuditAssociationQueryImpl setCacheMode(CacheMode cacheMode) { + public AbstractAuditAssociationQuery setCacheMode(CacheMode cacheMode) { parent.setCacheMode( cacheMode ); return this; } @Override - public AuditAssociationQueryImpl setTimeout(int timeout) { + public AbstractAuditAssociationQuery setTimeout(int timeout) { parent.setTimeout( timeout ); return this; } @Override - public AuditAssociationQueryImpl setLockMode(LockMode lockMode) { + public AbstractAuditAssociationQuery setLockMode(LockMode lockMode) { parent.setLockMode( lockMode ); return this; } @@ -294,7 +297,7 @@ public class AuditAssociationQueryImpl return parent; } - protected void addCriterionsToQuery(AuditReaderImplementor versionsReader) { + protected void addCriterionToQuery(AuditReaderImplementor versionsReader) { Parameters onClauseParameters; if ( relationDescription != null ) { onClauseParameters = createEntityJoin( enversService.getConfig() ); @@ -327,12 +330,12 @@ public class AuditAssociationQueryImpl ); } - for ( AuditAssociationQueryImpl query : associationQueries ) { - query.addCriterionsToQuery( versionsReader ); + for ( AbstractAuditAssociationQuery subQuery : associationQueries ) { + subQuery.addCriterionToQuery( versionsReader ); } } - private Parameters createEntityJoin(Configuration configuration) { + protected Parameters createEntityJoin(Configuration configuration) { boolean targetIsAudited = enversService.getEntitiesConfigurations().isVersioned( entityName ); String targetEntityName = entityName; if ( targetIsAudited ) { @@ -480,40 +483,10 @@ public class AuditAssociationQueryImpl ); } - if ( targetIsAudited ) { - // filter revision of target entity - Parameters parametersToUse = parameters; - if ( joinType == JoinType.LEFT ) { - parametersToUse = parameters.addSubParameters( Parameters.OR ); - parametersToUse.addNullRestriction( revisionPropertyPath, true ); - parametersToUse = parametersToUse.addSubParameters( Parameters.AND ); - } - MiddleIdData referencedIdData = new MiddleIdData( - configuration, - enversService.getEntitiesConfigurations().get( entityName ).getIdMappingData(), - null, - entityName, - true - ); - enversService.getAuditStrategy().addEntityAtRevisionRestriction( - configuration, - queryBuilder, - parametersToUse, - revisionPropertyPath, - configuration.getRevisionEndFieldName(), - true, - referencedIdData, - revisionPropertyPath, - originalIdPropertyName, - alias, - queryBuilder.generateAlias(), - true - ); - } return onClauseParameters; } - private Parameters createComponentJoin(Configuration configuration) { + protected Parameters createComponentJoin(Configuration configuration) { String originalIdPropertyName = configuration.getOriginalIdPropertyName(); String revisionPropertyPath = configuration.getRevisionNumberPath(); Parameters onClauseParameters; diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/AbstractAuditQuery.java b/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/AbstractAuditQuery.java index 76745e633f..17ad72e160 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/AbstractAuditQuery.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/AbstractAuditQuery.java @@ -58,7 +58,9 @@ public abstract class AbstractAuditQuery implements AuditQueryImplementor { protected final EnversService enversService; protected final AuditReaderImplementor versionsReader; - protected final List> associationQueries = new ArrayList<>(); + // todo: can these association query collections be merged? + protected final List> associationQueries = new ArrayList<>(); + protected final Map> associationQueryMap = new HashMap<>(); protected final List> projections = new ArrayList<>(); protected AbstractAuditQuery( @@ -202,29 +204,6 @@ public abstract class AbstractAuditQuery implements AuditQueryImplementor { null ); } - @Override - public AuditAssociationQuery traverseRelation( - String associationName, - JoinType joinType, - String alias, - AuditCriterion onClause) { - AuditAssociationQueryImpl result = new AuditAssociationQueryImpl<>( - enversService, - versionsReader, - this, - qb, - associationName, - joinType, - aliasToEntityNameMap, - aliasToComponentPropertyNameMap, - REFERENCED_ENTITY_ALIAS, - alias, - onClause - ); - associationQueries.add( result ); - return result; - } - // Query properties private Integer maxResults; @@ -383,4 +362,9 @@ public abstract class AbstractAuditQuery implements AuditQueryImplementor { // todo: can this be replaced by a call to getEntittyConfiguration#getEntityClassName()? return entityName; } + + protected void addAssociationQuery(String associationName, AbstractAuditAssociationQuery query) { + associationQueries.add( query ); + associationQueryMap.put( associationName, query ); + } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/EntitiesAtRevisionAssociationQuery.java b/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/EntitiesAtRevisionAssociationQuery.java new file mode 100644 index 0000000000..206c070ec7 --- /dev/null +++ b/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/EntitiesAtRevisionAssociationQuery.java @@ -0,0 +1,120 @@ +/* + * 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 . + */ +package org.hibernate.envers.query.internal.impl; + +import java.util.Map; + +import org.hibernate.Incubating; +import org.hibernate.envers.boot.internal.EnversService; +import org.hibernate.envers.configuration.Configuration; +import org.hibernate.envers.internal.entities.mapper.relation.MiddleIdData; +import org.hibernate.envers.internal.reader.AuditReaderImplementor; +import org.hibernate.envers.internal.tools.query.Parameters; +import org.hibernate.envers.internal.tools.query.QueryBuilder; +import org.hibernate.envers.query.AuditAssociationQuery; +import org.hibernate.envers.query.criteria.AuditCriterion; + +import jakarta.persistence.criteria.JoinType; + +/** + * An {@link AuditAssociationQuery} implementation for + * {@link EntitiesAtRevisionQuery} and {@link EntitiesModifiedAtRevisionQuery} query types. + * + * @author Chris Cranford + */ +@Incubating +public class EntitiesAtRevisionAssociationQuery extends AbstractAuditAssociationQuery { + + public EntitiesAtRevisionAssociationQuery( + EnversService enversService, + AuditReaderImplementor auditReader, + Q parent, + QueryBuilder queryBuilder, + String propertyName, + JoinType joinType, + Map aliasToEntityNameMap, + Map aliasToComponentPropertyNameMap, + String ownerAlias, + String userSuppliedAlias, + AuditCriterion onClauseCriterion) { + super( + enversService, + auditReader, + parent, + queryBuilder, + propertyName, + joinType, + aliasToEntityNameMap, + aliasToComponentPropertyNameMap, + ownerAlias, + userSuppliedAlias, + onClauseCriterion + ); + } + + @Override + protected AbstractAuditAssociationQuery> createAssociationQuery( + String associationName, + JoinType joinType, + String alias, + AuditCriterion onClause) { + return new EntitiesAtRevisionAssociationQuery<>( + enversService, + auditReader, + this, + queryBuilder, + associationName, + joinType, + aliasToEntityNameMap, + aliasToComponentPropertyNameMap, + this.alias, + alias, + onClauseCriterion + ); + } + + @Override + protected Parameters createEntityJoin(Configuration configuration) { + Parameters onClauseParameters = super.createEntityJoin( configuration ); + + if ( enversService.getEntitiesConfigurations().isVersioned( entityName ) ) { + final String originalIdPropertyName = configuration.getOriginalIdPropertyName(); + final String revisionPropertyPath = configuration.getRevisionNumberPath(); + + // filter revision of target entity + Parameters parametersToUse = parameters; + if ( joinType == JoinType.LEFT ) { + parametersToUse = parameters.addSubParameters( Parameters.OR ); + parametersToUse.addNullRestriction( revisionPropertyPath, true ); + parametersToUse = parametersToUse.addSubParameters( Parameters.AND ); + } + MiddleIdData referencedIdData = new MiddleIdData( + configuration, + enversService.getEntitiesConfigurations().get( entityName ).getIdMappingData(), + null, + entityName, + true + ); + enversService.getAuditStrategy().addEntityAtRevisionRestriction( + configuration, + queryBuilder, + parametersToUse, + revisionPropertyPath, + configuration.getRevisionEndFieldName(), + true, + referencedIdData, + revisionPropertyPath, + originalIdPropertyName, + alias, + queryBuilder.generateAlias(), + true + ); + } + + return onClauseParameters; + } +} diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/EntitiesAtRevisionQuery.java b/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/EntitiesAtRevisionQuery.java index 919d54b7ff..c34b21de43 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/EntitiesAtRevisionQuery.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/EntitiesAtRevisionQuery.java @@ -9,12 +9,16 @@ package org.hibernate.envers.query.internal.impl; import java.util.Collection; import java.util.List; +import jakarta.persistence.criteria.JoinType; + import org.hibernate.envers.RevisionType; import org.hibernate.envers.boot.internal.EnversService; import org.hibernate.envers.configuration.Configuration; import org.hibernate.envers.internal.entities.mapper.relation.MiddleIdData; import org.hibernate.envers.internal.entities.mapper.relation.query.QueryConstants; import org.hibernate.envers.internal.reader.AuditReaderImplementor; +import org.hibernate.envers.query.AuditAssociationQuery; +import org.hibernate.envers.query.AuditQuery; import org.hibernate.envers.query.criteria.AuditCriterion; import org.hibernate.query.Query; @@ -121,8 +125,8 @@ public class EntitiesAtRevisionQuery extends AbstractAuditQuery { ); } - for (final AuditAssociationQueryImpl associationQuery : associationQueries) { - associationQuery.addCriterionsToQuery( versionsReader ); + for ( AbstractAuditAssociationQuery associationQuery : associationQueries ) { + associationQuery.addCriterionToQuery( versionsReader ); } Query query = buildQuery(); @@ -134,4 +138,30 @@ public class EntitiesAtRevisionQuery extends AbstractAuditQuery { List queryResult = query.list(); return applyProjections( queryResult, revision ); } + + @Override + public AuditAssociationQuery traverseRelation( + String associationName, + JoinType joinType, + String alias, + AuditCriterion onClauseCriterion) { + AbstractAuditAssociationQuery query = associationQueryMap.get( associationName ); + if ( query == null ) { + query = new EntitiesAtRevisionAssociationQuery<>( + enversService, + versionsReader, + this, + qb, + associationName, + joinType, + aliasToEntityNameMap, + aliasToComponentPropertyNameMap, + REFERENCED_ENTITY_ALIAS, + alias, + onClauseCriterion + ); + addAssociationQuery( associationName, query ); + } + return query; + } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/EntitiesModifiedAtRevisionQuery.java b/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/EntitiesModifiedAtRevisionQuery.java index 23e7a97188..b8cc656955 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/EntitiesModifiedAtRevisionQuery.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/EntitiesModifiedAtRevisionQuery.java @@ -9,12 +9,17 @@ package org.hibernate.envers.query.internal.impl; import java.util.Collection; import java.util.List; +import jakarta.persistence.criteria.JoinType; + import org.hibernate.envers.boot.internal.EnversService; import org.hibernate.envers.internal.entities.mapper.relation.query.QueryConstants; import org.hibernate.envers.internal.reader.AuditReaderImplementor; +import org.hibernate.envers.query.AuditAssociationQuery; +import org.hibernate.envers.query.AuditQuery; import org.hibernate.envers.query.criteria.AuditCriterion; import org.hibernate.query.Query; +import static org.hibernate.envers.internal.entities.mapper.relation.query.QueryConstants.REFERENCED_ENTITY_ALIAS; import static org.hibernate.envers.internal.entities.mapper.relation.query.QueryConstants.REVISION_PARAMETER; /** @@ -73,8 +78,8 @@ public class EntitiesModifiedAtRevisionQuery extends AbstractAuditQuery { ); } - for (final AuditAssociationQueryImpl associationQuery : associationQueries) { - associationQuery.addCriterionsToQuery( versionsReader ); + for ( AbstractAuditAssociationQuery associationQuery : associationQueries ) { + associationQuery.addCriterionToQuery( versionsReader ); } Query query = buildQuery(); @@ -86,4 +91,30 @@ public class EntitiesModifiedAtRevisionQuery extends AbstractAuditQuery { List queryResult = query.list(); return applyProjections( queryResult, revision ); } + + @Override + public AuditAssociationQuery traverseRelation( + String associationName, + JoinType joinType, + String alias, + AuditCriterion onClauseCriterion) { + AbstractAuditAssociationQuery query = associationQueryMap.get( associationName ); + if ( query == null ) { + query = new EntitiesAtRevisionAssociationQuery<>( + enversService, + versionsReader, + this, + qb, + associationName, + joinType, + aliasToEntityNameMap, + aliasToComponentPropertyNameMap, + REFERENCED_ENTITY_ALIAS, + alias, + null + ); + addAssociationQuery( associationName, query ); + } + return query; + } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/RevisionsOfEntityAssociationQuery.java b/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/RevisionsOfEntityAssociationQuery.java new file mode 100644 index 0000000000..3606320976 --- /dev/null +++ b/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/RevisionsOfEntityAssociationQuery.java @@ -0,0 +1,76 @@ +/* + * 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 . + */ +package org.hibernate.envers.query.internal.impl; + +import java.util.Map; + +import org.hibernate.Incubating; +import org.hibernate.envers.boot.internal.EnversService; +import org.hibernate.envers.internal.reader.AuditReaderImplementor; +import org.hibernate.envers.internal.tools.query.QueryBuilder; +import org.hibernate.envers.query.AuditAssociationQuery; +import org.hibernate.envers.query.criteria.AuditCriterion; + +import jakarta.persistence.criteria.JoinType; + +/** + * An {@link AuditAssociationQuery} implementation for {@link RevisionsOfEntityQuery}. + * + * @author Chris Cranford + */ +@Incubating +public class RevisionsOfEntityAssociationQuery extends AbstractAuditAssociationQuery { + + public RevisionsOfEntityAssociationQuery( + EnversService enversService, + AuditReaderImplementor auditReader, + Q parent, + QueryBuilder queryBuilder, + String propertyName, + JoinType joinType, + Map aliasToEntityNameMap, + Map aliastoComponentPropertyNameMap, + String ownerAlias, + String userSuppliedAlias, + AuditCriterion onClauseCriterion) { + super( + enversService, + auditReader, + parent, + queryBuilder, + propertyName, + joinType, + aliasToEntityNameMap, + aliastoComponentPropertyNameMap, + ownerAlias, + userSuppliedAlias, + onClauseCriterion + ); + } + + @Override + protected AbstractAuditAssociationQuery> createAssociationQuery( + String associationName, + JoinType joinType, + String alias, + AuditCriterion onClauseCriterion) { + return new RevisionsOfEntityAssociationQuery<>( + enversService, + auditReader, + this, + queryBuilder, + associationName, + joinType, + aliasToEntityNameMap, + aliasToComponentPropertyNameMap, + this.alias, + alias, + onClauseCriterion + ); + } + +} diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/RevisionsOfEntityQuery.java b/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/RevisionsOfEntityQuery.java index 6524bee345..9edb2d5a3e 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/RevisionsOfEntityQuery.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/query/internal/impl/RevisionsOfEntityQuery.java @@ -6,6 +6,8 @@ */ package org.hibernate.envers.query.internal.impl; +import static org.hibernate.envers.internal.entities.mapper.relation.query.QueryConstants.REFERENCED_ENTITY_ALIAS; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; @@ -72,6 +74,40 @@ public class RevisionsOfEntityQuery extends AbstractAuditQuery { this.includePropertyChanges = includePropertyChanges; } + @Override + public AuditAssociationQuery traverseRelation( + String associationName, + JoinType joinType, + String alias, + AuditCriterion onClauseCriterion) { + if ( !selectEntitiesOnly ) { + throw new IllegalStateException( + "Audit association queries are only permitted when the query is created with selectEntitiesOnly=true" + ); + } + + AbstractAuditAssociationQuery query = associationQueryMap.get( associationName ); + if ( query == null ) { + query = new RevisionsOfEntityAssociationQuery<>( + enversService, + versionsReader, + this, + qb, + associationName, + joinType, + aliasToEntityNameMap, + aliasToComponentPropertyNameMap, + REFERENCED_ENTITY_ALIAS, + alias, + null + ); + + addAssociationQuery( associationName, query ); + } + + return query; + } + private Number getRevisionNumber(Map versionsEntity) { Configuration configuration = enversService.getConfig(); @@ -89,7 +125,8 @@ public class RevisionsOfEntityQuery extends AbstractAuditQuery { } } - @SuppressWarnings({"unchecked"}) + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override public List list() throws AuditException { Configuration configuration = enversService.getConfig(); @@ -119,6 +156,10 @@ public class RevisionsOfEntityQuery extends AbstractAuditQuery { ); } + for ( AbstractAuditAssociationQuery associationQuery : associationQueries ) { + associationQuery.addCriterionToQuery( versionsReader ); + } + if ( !hasProjection() && !hasOrder ) { String revisionPropertyPath = configuration.getRevisionNumberPath(); qb.addOrder( QueryConstants.REFERENCED_ENTITY_ALIAS, revisionPropertyPath, true, null ); @@ -138,11 +179,6 @@ public class RevisionsOfEntityQuery extends AbstractAuditQuery { return getQueryResults(); } - @Override - public AuditAssociationQuery traverseRelation(String associationName, JoinType joinType) { - throw new UnsupportedOperationException( "Not yet implemented for revisions of entity queries" ); - } - private boolean isEntityUsingModifiedFlags() { // todo: merge HHH-8973 ModifiedFlagMapperSupport into 6.0 to get this behavior by default final ExtendedPropertyMapper propertyMapper = getEntityConfiguration().getPropertyMapper(); diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/BaseEnversFunctionalTestCase.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/BaseEnversFunctionalTestCase.java index 4364ad2dc1..9e7946e2d4 100644 --- a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/BaseEnversFunctionalTestCase.java +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/BaseEnversFunctionalTestCase.java @@ -13,10 +13,15 @@ import java.util.Map; import org.hibernate.Session; import org.hibernate.envers.AuditReader; import org.hibernate.envers.AuditReaderFactory; +import org.hibernate.envers.boot.internal.EnversService; +import org.hibernate.envers.configuration.Configuration; import org.hibernate.envers.configuration.EnversSettings; +import org.hibernate.internal.SessionImpl; import org.hibernate.resource.transaction.spi.TransactionStatus; import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; + +import org.hibernate.service.ServiceRegistry; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -61,6 +66,11 @@ public abstract class BaseEnversFunctionalTestCase extends BaseNonConfigCoreFunc return AuditReaderFactory.get( getSession() ); } + protected Configuration getConfiguration() { + ServiceRegistry registry = getSession().unwrap(SessionImpl.class ).getSessionFactory().getServiceRegistry(); + return registry.getService( EnversService.class ).getConfig(); + } + @Override protected void addSettings(Map settings) { super.addSettings( settings ); diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/query/AssociationRevisionsOfEntitiesQueryStoreAtDeletionTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/query/AssociationRevisionsOfEntitiesQueryStoreAtDeletionTest.java new file mode 100644 index 0000000000..7bf5fa3eb5 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/query/AssociationRevisionsOfEntitiesQueryStoreAtDeletionTest.java @@ -0,0 +1,25 @@ +/* + * 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 . + */ +package org.hibernate.orm.test.envers.integration.query; + +import java.util.Map; + +import org.hibernate.envers.configuration.EnversSettings; + +import org.hibernate.testing.TestForIssue; + +/** + * @author Chris Cranford + */ +@TestForIssue( jiraKey = "HHH-13817" ) +public class AssociationRevisionsOfEntitiesQueryStoreAtDeletionTest extends AssociationRevisionsOfEntitiesQueryTest { + @Override + protected void addSettings(Map settings) { + super.addSettings( settings ); + settings.put( EnversSettings.STORE_DATA_AT_DELETE, true ); + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/query/AssociationRevisionsOfEntitiesQueryTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/query/AssociationRevisionsOfEntitiesQueryTest.java new file mode 100644 index 0000000000..2f5f4cb260 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/query/AssociationRevisionsOfEntitiesQueryTest.java @@ -0,0 +1,192 @@ +/* + * 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 . + */ +package org.hibernate.orm.test.envers.integration.query; + +import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping; +import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import java.util.List; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.criteria.JoinType; + +import org.hibernate.envers.Audited; +import org.hibernate.envers.query.AuditEntity; +import org.hibernate.orm.test.envers.BaseEnversFunctionalTestCase; +import org.hibernate.orm.test.envers.Priority; +import org.junit.Test; + +import org.hibernate.testing.TestForIssue; + +/** + * @author Chris Cranford + */ +@TestForIssue( jiraKey = "HHH-13817" ) +public class AssociationRevisionsOfEntitiesQueryTest extends BaseEnversFunctionalTestCase { + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Template.class, TemplateType.class }; + } + + @Test + @Priority(10) + public void initData() { + doInHibernate( this::sessionFactory, session -> { + final TemplateType type1 = new TemplateType( 1, "Type1" ); + final TemplateType type2 = new TemplateType( 2, "Type2" ); + session.save( type1 ); + session.save( type2 ); + + final Template template = new Template( 1, "Template1", type1 ); + session.save( template ); + } ); + + doInHibernate( this::sessionFactory, session -> { + final TemplateType type = session.find( TemplateType.class, 2 ); + final Template template = session.find( Template.class, 1 ); + template.setName( "Template1-Updated" ); + template.setTemplateType( type ); + session.update( template ); + } ); + + doInHibernate( this::sessionFactory, session -> { + final Template template = session.find( Template.class, 1 ); + session.remove( template ); + } ); + } + + @Test + public void testRevisionsOfEntityWithAssociationQueries() { + doInHibernate( this::sessionFactory, session -> { + List results = getAuditReader().createQuery() + .forRevisionsOfEntity( Template.class, true, true ) + .add( AuditEntity.id().eq( 1 ) ) + .traverseRelation( "templateType", JoinType.INNER ) + .add( AuditEntity.property( "name" ).eq( "Type1" ) ) + .up() + .getResultList(); + assertEquals( 1, results.size() ); + assertEquals( "Template1", ( (Template) results.get( 0 ) ).getName() ); + } ); + + doInHibernate( this::sessionFactory, session -> { + List results = getAuditReader().createQuery() + .forRevisionsOfEntity( Template.class, true, true ) + .add( AuditEntity.id().eq( 1 ) ) + .traverseRelation( "templateType", JoinType.INNER ) + .add( AuditEntity.property("name" ).eq("Type2" ) ) + .up() + .getResultList(); + + assertEquals( getConfiguration().isStoreDataAtDelete() ? 2 : 1, results.size() ); + for ( Object result : results ) { + assertEquals( "Template1-Updated", ( (Template) result ).getName() ); + } + } ); + } + + @Test + public void testAssociationQueriesNotAllowedWhenNotSelectingJustEntities() { + try { + doInHibernate( this::sessionFactory, session -> { + getAuditReader().createQuery() + .forRevisionsOfEntity( Template.class, false, true ) + .add( AuditEntity.id().eq( 1 ) ) + .traverseRelation("templateType", JoinType.INNER ) + .add( AuditEntity.property( "name" ).eq( "Type1" ) ) + .up() + .getResultList(); + } ); + + fail( "Test should have thrown IllegalStateException due to selectEntitiesOnly=false" ); + } + catch ( Exception e ) { + assertTyping( IllegalStateException.class, e ); + } + } + + @Entity(name = "TemplateType") + @Audited + public static class TemplateType { + @Id + private Integer id; + private String name; + + TemplateType() { + this( null, null ); + } + + TemplateType(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity(name = "Template") + @Audited + public static class Template { + @Id + private Integer id; + private String name; + @ManyToOne + private TemplateType templateType; + + Template() { + this( null, null, null ); + } + + Template(Integer id, String name, TemplateType type) { + this.id = id; + this.name = name; + this.templateType = type; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public TemplateType getTemplateType() { + return templateType; + } + + public void setTemplateType(TemplateType templateType) { + this.templateType = templateType; + } + } +}