HHH-11896 Support 'on-clause' criterion when traversing audit query relations

This commit is contained in:
Felix Feisst 2017-07-28 10:13:33 -04:00 committed by Chris Cranford
parent e07a8c3bd5
commit 3e3d227c9a
5 changed files with 360 additions and 44 deletions

View File

@ -13,6 +13,7 @@ import jakarta.persistence.criteria.JoinType;
import org.hibernate.CacheMode;
import org.hibernate.FlushMode;
import org.hibernate.Incubating;
import org.hibernate.LockMode;
import org.hibernate.envers.exception.AuditException;
import org.hibernate.envers.query.criteria.AuditCriterion;
@ -21,7 +22,6 @@ import org.hibernate.envers.query.projection.AuditProjection;
/**
* @author Adam Warski (adam at warski dot org)
* @see org.hibernate.Criteria
*/
public interface AuditQuery {
List getResultList() throws AuditException;
@ -30,9 +30,18 @@ public interface AuditQuery {
AuditAssociationQuery<? extends AuditQuery> traverseRelation(String associationName, JoinType joinType);
AuditAssociationQuery<? extends AuditQuery> traverseRelation(String associationName, JoinType joinType,
AuditAssociationQuery<? extends AuditQuery> traverseRelation(
String associationName,
JoinType joinType,
String alias);
@Incubating
AuditAssociationQuery<? extends AuditQuery> traverseRelation(
String associationName,
JoinType joinType,
String alias,
AuditCriterion onClauseCriterion);
AuditQuery add(AuditCriterion criterion);
AuditQuery addProjection(AuditProjection projection);

View File

@ -59,7 +59,6 @@ public abstract class AbstractAuditQuery implements AuditQueryImplementor {
protected final AuditReaderImplementor versionsReader;
protected final List<AuditAssociationQueryImpl<?>> associationQueries = new ArrayList<>();
protected final Map<String, AuditAssociationQueryImpl<AuditQueryImplementor>> associationQueryMap = new HashMap<>();
protected final List<Pair<String, AuditProjection>> projections = new ArrayList<>();
protected AbstractAuditQuery(
@ -196,23 +195,33 @@ public abstract class AbstractAuditQuery implements AuditQueryImplementor {
@Override
public AuditAssociationQuery<? extends AuditQuery> traverseRelation(String associationName, JoinType joinType, String alias) {
AuditAssociationQueryImpl<AuditQueryImplementor> result = associationQueryMap.get( associationName );
if (result == null) {
result = new AuditAssociationQueryImpl<>(
enversService,
versionsReader,
this,
qb,
associationName,
joinType,
aliasToEntityNameMap,
aliasToComponentPropertyNameMap,
REFERENCED_ENTITY_ALIAS,
alias
);
associationQueries.add( result );
associationQueryMap.put( associationName, result );
}
return traverseRelation(
associationName,
joinType,
alias,
null );
}
@Override
public AuditAssociationQuery<? extends AuditQuery> traverseRelation(
String associationName,
JoinType joinType,
String alias,
AuditCriterion onClause) {
AuditAssociationQueryImpl<AbstractAuditQuery> result = new AuditAssociationQueryImpl<>(
enversService,
versionsReader,
this,
qb,
associationName,
joinType,
aliasToEntityNameMap,
aliasToComponentPropertyNameMap,
REFERENCED_ENTITY_ALIAS,
alias,
onClause
);
associationQueries.add( result );
return result;
}

View File

@ -7,7 +7,6 @@
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;
@ -60,9 +59,9 @@ public class AuditAssociationQueryImpl<Q extends AuditQueryImplementor>
private final Map<String, String> aliasToEntityNameMap;
private final Map<String, String> aliasToComponentPropertyNameMap;
private final List<AuditCriterion> criterions = new ArrayList<>();
private final AuditCriterion onClauseCriterion;
private final Parameters parameters;
private final List<AuditAssociationQueryImpl<?>> associationQueries = new ArrayList<>();
private final Map<String, AuditAssociationQueryImpl<AuditAssociationQueryImpl<Q>>> associationQueryMap = new HashMap<>();
public AuditAssociationQueryImpl(
final EnversService enversService,
@ -74,7 +73,8 @@ public class AuditAssociationQueryImpl<Q extends AuditQueryImplementor>
final Map<String, String> aliasToEntityNameMap,
final Map<String, String> aliasToComponentPropertyNameMap,
final String ownerAlias,
final String userSuppliedAlias) {
final String userSuppliedAlias,
final AuditCriterion onClauseCriterion) {
this.enversService = enversService;
this.auditReader = auditReader;
this.parent = parent;
@ -123,6 +123,7 @@ public class AuditAssociationQueryImpl<Q extends AuditQueryImplementor>
this.aliasToEntityNameMap = aliasToEntityNameMap;
this.aliasToComponentPropertyNameMap = aliasToComponentPropertyNameMap;
parameters = queryBuilder.addParameters( this.alias );
this.onClauseCriterion = onClauseCriterion;
}
@Override
@ -156,23 +157,34 @@ public class AuditAssociationQueryImpl<Q extends AuditQueryImplementor>
String associationName,
JoinType joinType,
String alias) {
AuditAssociationQueryImpl<AuditAssociationQueryImpl<Q>> result = associationQueryMap.get( associationName );
if ( result == null ) {
result = new AuditAssociationQueryImpl<>(
enversService,
auditReader,
this,
queryBuilder,
associationName,
joinType,
aliasToEntityNameMap,
aliasToComponentPropertyNameMap,
this.alias,
alias
);
associationQueries.add( result );
associationQueryMap.put( associationName, result );
}
return traverseRelation(
associationName,
joinType,
alias,
null
);
}
@Override
public AuditAssociationQueryImpl<AuditAssociationQueryImpl<Q>> traverseRelation(
String associationName,
JoinType joinType,
String alias,
AuditCriterion onClause) {
AuditAssociationQueryImpl<AuditAssociationQueryImpl<Q>> result = new AuditAssociationQueryImpl<>(
enversService,
auditReader,
this,
queryBuilder,
associationName,
joinType,
aliasToEntityNameMap,
aliasToComponentPropertyNameMap,
this.alias,
alias,
onClause
);
associationQueries.add( result );
return result;
}
@ -283,11 +295,24 @@ public class AuditAssociationQueryImpl<Q extends AuditQueryImplementor>
}
protected void addCriterionsToQuery(AuditReaderImplementor versionsReader) {
Parameters onClauseParameters;
if ( relationDescription != null ) {
createEntityJoin( enversService.getConfig() );
onClauseParameters = createEntityJoin( enversService.getConfig() );
}
else {
createComponentJoin( enversService.getConfig() );
onClauseParameters = createComponentJoin( enversService.getConfig() );
}
if ( onClauseCriterion != null ) {
onClauseCriterion.addToQuery(
enversService,
versionsReader,
aliasToEntityNameMap,
aliasToComponentPropertyNameMap,
alias,
queryBuilder,
onClauseParameters
);
}
for ( AuditCriterion criterion : criterions ) {
@ -307,7 +332,7 @@ public class AuditAssociationQueryImpl<Q extends AuditQueryImplementor>
}
}
private void createEntityJoin(Configuration configuration) {
private Parameters createEntityJoin(Configuration configuration) {
boolean targetIsAudited = enversService.getEntitiesConfigurations().isVersioned( entityName );
String targetEntityName = entityName;
if ( targetIsAudited ) {
@ -316,8 +341,10 @@ public class AuditAssociationQueryImpl<Q extends AuditQueryImplementor>
String originalIdPropertyName = configuration.getOriginalIdPropertyName();
String revisionPropertyPath = configuration.getRevisionNumberPath();
Parameters onClauseParameters;
if ( relationDescription.getRelationType() == RelationType.TO_ONE ) {
Parameters joinConditionParameters = queryBuilder.addJoin( joinType, targetEntityName, alias, false );
onClauseParameters = joinConditionParameters;
// owner.reference_id = target.originalId.id
IdMapper idMapperTarget;
@ -350,6 +377,7 @@ public class AuditAssociationQueryImpl<Q extends AuditQueryImplementor>
);
}
Parameters joinConditionParameters = queryBuilder.addJoin( joinType, targetEntityName, alias, false );
onClauseParameters = joinConditionParameters;
// owner.originalId.id = target.reference_id
IdMapper idMapperOwner = enversService.getEntitiesConfigurations().get( ownerEntityName ).getIdMapper();
@ -384,6 +412,7 @@ public class AuditAssociationQueryImpl<Q extends AuditQueryImplementor>
// join target_entity
Parameters joinConditionParametersTarget = queryBuilder.addJoin( joinType, targetEntityName, alias, false );
onClauseParameters = joinConditionParametersTarget;
Parameters middleParameters = queryBuilder.addParameters( middleEntityAlias );
String middleOriginalIdPropertyPath = middleEntityAlias + "." + originalIdPropertyName;
@ -481,11 +510,13 @@ public class AuditAssociationQueryImpl<Q extends AuditQueryImplementor>
true
);
}
return onClauseParameters;
}
private void createComponentJoin(Configuration configuration) {
private Parameters createComponentJoin(Configuration configuration) {
String originalIdPropertyName = configuration.getOriginalIdPropertyName();
String revisionPropertyPath = configuration.getRevisionNumberPath();
Parameters onClauseParameters;
if ( componentDescription.getType() == ComponentType.MANY ) {
// join middle_entity
Parameters joinConditionParameters = queryBuilder.addJoin(
@ -494,8 +525,10 @@ public class AuditAssociationQueryImpl<Q extends AuditQueryImplementor>
alias,
false
);
onClauseParameters = joinConditionParameters;
String middleOriginalIdPropertyPath = alias + "." + originalIdPropertyName;
// join condition: owner.reference_id = middle.id_ref_ing
String ownerPrefix = ownerAlias + "." + originalIdPropertyName;
MiddleIdData middleIdData = componentDescription.getMiddleIdData();
@ -550,6 +583,7 @@ public class AuditAssociationQueryImpl<Q extends AuditQueryImplementor>
*/
String targetEntityName = configuration.getAuditEntityName( entityName );
Parameters joinConditionParameters = queryBuilder.addJoin( joinType, targetEntityName, alias, false );
onClauseParameters = joinConditionParameters;
// join condition: owner.reference_id = middle.id_reference_id
String ownerPrefix = ownerAlias + "." + originalIdPropertyName;
@ -560,6 +594,7 @@ public class AuditAssociationQueryImpl<Q extends AuditQueryImplementor>
// join condition: owner.rev=middle.rev
joinConditionParameters.addWhere( ownerAlias, revisionPropertyPath, "=", alias, revisionPropertyPath );
}
return onClauseParameters;
}
@Override

View File

@ -0,0 +1,232 @@
/*
* 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.orm.test.envers.integration.query;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import jakarta.persistence.Entity;
import jakarta.persistence.EntityManager;
import jakarta.persistence.Id;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.criteria.JoinType;
import org.hibernate.envers.AuditJoinTable;
import org.hibernate.envers.Audited;
import org.hibernate.envers.query.AuditEntity;
import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase;
import org.hibernate.orm.test.envers.Priority;
import org.hibernate.testing.TestForIssue;
import org.junit.Test;
import static org.junit.Assert.*;
/**
* @author Felix Feisst (feisst dot felix at gmail dot com)
*/
@TestForIssue(jiraKey = "HHH-11896")
public class AssociationQueryWithOnClauseTest extends BaseEnversJPAFunctionalTestCase {
private EntityA a1;
private EntityA a2;
private EntityA a3;
@Entity(name = "EntityA")
@Audited
public static class EntityA {
@Id
private Long id;
@ManyToOne
private EntityB bManyToOne;
@OneToMany
@AuditJoinTable(name = "entitya_onetomany_entityb_aud")
private Set<EntityB> bOneToMany = new HashSet<>();
@ManyToMany
@JoinTable(name = "entitya_manytomany_entityb")
private Set<EntityB> bManyToMany = new HashSet<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public EntityB getbManyToOne() {
return bManyToOne;
}
public void setbManyToOne(EntityB bManyToOne) {
this.bManyToOne = bManyToOne;
}
public Set<EntityB> getbOneToMany() {
return bOneToMany;
}
public void setbOneToMany(Set<EntityB> bOneToMany) {
this.bOneToMany = bOneToMany;
}
public Set<EntityB> getbManyToMany() {
return bManyToMany;
}
public void setbManyToMany(Set<EntityB> bManyToMany) {
this.bManyToMany = bManyToMany;
}
}
@Entity(name = "EntityB")
@Audited
public static class EntityB {
@Id
private Long id;
private String type;
private int number;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
}
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[]{ EntityA.class, EntityB.class };
}
@Test
@Priority(10)
public void initData() {
EntityManager em = getEntityManager();
em.getTransaction().begin();
final EntityB b1t1 = new EntityB();
b1t1.setId( 21L );
b1t1.setType( "T1" );
b1t1.setNumber( 1 );
em.persist( b1t1 );
final EntityB b2t2 = new EntityB();
b2t2.setId( 22L );
b2t2.setType( "T2" );
b2t2.setNumber( 2 );
em.persist( b2t2 );
final EntityB b3t1 = new EntityB();
b3t1.setId( 23L );
b3t1.setType( "T1" );
b3t1.setNumber( 3 );
em.persist( b3t1 );
a1 = new EntityA();
a1.setId( 1L );
a1.setbManyToOne( b1t1 );
a1.getbOneToMany().add( b1t1 );
a1.getbOneToMany().add( b2t2 );
a1.getbManyToMany().add( b1t1 );
a1.getbManyToMany().add( b2t2 );
em.persist( a1 );
a2 = new EntityA();
a2.setId( 2L );
a2.setbManyToOne( b2t2 );
a2.getbManyToMany().add( b3t1 );
em.persist( a2 );
a3 = new EntityA();
a3.setId( 3L );
a3.setbManyToOne( b3t1 );
a3.getbOneToMany().add( b3t1 );
a3.getbManyToMany().add( b3t1 );
em.persist( a3 );
em.getTransaction().commit();
}
@Test
public void testManyToOne() {
List list = getAuditReader().createQuery()
.forEntitiesAtRevision( EntityA.class, 1 )
.traverseRelation( "bManyToOne", JoinType.LEFT, "b", AuditEntity.property( "b", "type" ).eq( "T1" ) )
.addOrder( AuditEntity.property( "b", "number" ).asc() )
.up()
.addProjection( AuditEntity.id() )
.addProjection( AuditEntity.property( "b", "number" ) )
.getResultList();
assertArrayListEquals( list, tuple( a2.getId(), null ), tuple( a1.getId(), 1 ), tuple( a3.getId(), 3 ) );
}
@Test
public void testOneToMany() {
List list = getAuditReader().createQuery().forEntitiesAtRevision( EntityA.class, 1 )
.traverseRelation( "bOneToMany", JoinType.LEFT, "b", AuditEntity.property( "b", "type" ).eq( "T1" ) )
.addOrder( AuditEntity.property( "b", "number" ).asc() )
.up()
.addOrder( AuditEntity.id().asc() )
.addProjection( AuditEntity.id() )
.addProjection( AuditEntity.property( "b", "number" ) )
.getResultList();
assertArrayListEquals( list, tuple( a1.getId(), null ), tuple( a2.getId(), null ), tuple( a1.getId(), 1 ), tuple( a3.getId(), 3 ) );
}
@Test
public void testManyToMany() {
List list = getAuditReader().createQuery()
.forEntitiesAtRevision( EntityA.class, 1 )
.traverseRelation( "bManyToMany", JoinType.LEFT, "b", AuditEntity.property( "b", "type" ).eq( "T1" ) )
.addOrder( AuditEntity.property( "b", "number" ).asc() )
.up()
.addOrder( AuditEntity.id().asc() ).addProjection( AuditEntity.id() )
.addProjection( AuditEntity.property( "b", "number" ) )
.getResultList();
assertArrayListEquals( list, tuple( a1.getId(), null ), tuple( a1.getId(), 1 ), tuple( a2.getId(), 3 ), tuple( a3.getId(), 3 ) );
}
private Object[] tuple(final Long id, final Integer number) {
return new Object[]{ id, number };
}
private void assertArrayListEquals(final List actual, final Object[]... expected) {
assertEquals( "Unexpected number of results", expected.length, actual.size() );
for ( int i = 0; i < expected.length; i++ ) {
final Object[] exp = expected[i];
final Object[] act = (Object[]) actual.get( i );
assertArrayEquals( exp, act );
}
}
}

View File

@ -334,4 +334,35 @@ public class ComponentQueryTest extends BaseEnversJPAFunctionalTestCase {
assertEquals( "Expecte the symbol identfier of asset2 concatenated with 'Z'", Collections.singletonList( "XZ" ), actual );
}
@Test
@TestForIssue(jiraKey = "HHH-11896")
public void testOnClauseOnSingleSymbol() {
List actual = getAuditReader().createQuery()
.forEntitiesAtRevision( Asset.class, 1 )
.addProjection( AuditEntity.id() )
.traverseRelation( "singleSymbol", JoinType.LEFT, "s", AuditEntity.property( "s", "type" ).eq( type1 ) )
.addOrder( AuditEntity.property( "s", "identifier" ).asc() )
.up()
.addOrder( AuditEntity.id().asc() )
.getResultList();
final List<Integer> expected = new ArrayList<>();
Collections.addAll( expected, asset1.getId(), asset3.getId(), asset2.getId() );
assertEquals( "Expected the correct ordering. Assets which do not have a symbol of type1 should come first (null first)", expected, actual );
}
@Test
@TestForIssue(jiraKey = "HHH-11896")
public void testOnClauseOnMultiSymbol() {
List actual = getAuditReader().createQuery()
.forEntitiesAtRevision( Asset.class, 1 )
.addProjection( AuditEntity.id() )
.traverseRelation( "multiSymbols", JoinType.LEFT, "s", AuditEntity.property( "s", "type" ).eq( type1 ) )
.addOrder( AuditEntity.property( "s", "identifier" ).asc() )
.up()
.addOrder( AuditEntity.id().asc() )
.getResultList();
final List<Integer> expected = new ArrayList<>();
Collections.addAll( expected, asset1.getId(), asset2.getId(), asset3.getId() );
assertEquals( "Expected the correct ordering. Assets which do not have a symbol of type1 should come first (null first)", expected, actual );
}
}