HHH-14981 Support null precedence with Envers Query API

This commit is contained in:
Chris Cranford 2021-12-16 05:06:33 -05:00 committed by Chris Cranford
parent bc65526c77
commit 25421733d6
8 changed files with 163 additions and 12 deletions

View File

@ -30,6 +30,7 @@ import org.hibernate.envers.internal.tools.Triple;
import org.hibernate.envers.query.criteria.AuditFunction; import org.hibernate.envers.query.criteria.AuditFunction;
import org.hibernate.envers.query.criteria.AuditId; import org.hibernate.envers.query.criteria.AuditId;
import org.hibernate.envers.query.criteria.AuditProperty; import org.hibernate.envers.query.criteria.AuditProperty;
import org.hibernate.envers.query.order.NullPrecedence;
import org.hibernate.envers.tools.Pair; import org.hibernate.envers.tools.Pair;
import org.hibernate.query.Query; import org.hibernate.query.Query;
import org.hibernate.query.internal.QueryLiteralHelper; import org.hibernate.query.internal.QueryLiteralHelper;
@ -63,9 +64,9 @@ public class QueryBuilder {
*/ */
private final List<JoinParameter> froms; private final List<JoinParameter> froms;
/** /**
* A list of triples (alias, property name, order ascending?). * A list of order by clauses.
*/ */
private final List<Triple<String, String, Boolean>> orders; private final List<OrderByClause> orders;
/** /**
* A list of complete projection definitions: either a sole property name, or a function(property name). * A list of complete projection definitions: either a sole property name, or a function(property name).
*/ */
@ -198,8 +199,8 @@ public class QueryBuilder {
return result; return result;
} }
public void addOrder(String alias, String propertyName, boolean ascending) { public void addOrder(String alias, String propertyName, boolean ascending, NullPrecedence nullPrecedence) {
orders.add( Triple.make( alias, propertyName, ascending ) ); orders.add( new OrderByClause( alias, propertyName, ascending, nullPrecedence ) );
} }
public void addOrderFragment(String alias, String orderByCollectionRole) { public void addOrderFragment(String alias, String orderByCollectionRole) {
@ -382,10 +383,9 @@ public class QueryBuilder {
private List<String> getOrderList() { private List<String> getOrderList() {
final List<String> orderList = new ArrayList<>(); final List<String> orderList = new ArrayList<>();
for ( Triple<String, String, Boolean> order : orders ) { for ( OrderByClause orderByClause : orders ) {
orderList.add( order.getFirst() + "." + order.getSecond() + " " + (order.getThird() ? "asc" : "desc") ); orderList.add( orderByClause.renderToHql() );
} }
return orderList; return orderList;
} }
@ -481,4 +481,32 @@ public class QueryBuilder {
} }
private static class OrderByClause {
private String alias;
private String propertyName;
private boolean ascending;
private NullPrecedence nullPrecedence;
public OrderByClause(String alias, String propertyName, boolean ascending, NullPrecedence nullPrecedence) {
this.alias = alias;
this.propertyName = propertyName;
this.ascending = ascending;
this.nullPrecedence = nullPrecedence;
}
public String renderToHql() {
StringBuilder hql = new StringBuilder();
hql.append( alias ).append( "." ).append( propertyName ).append( " " );
hql.append( ascending ? "asc" : "desc" );
if ( nullPrecedence != null ) {
if ( NullPrecedence.FIRST.equals( nullPrecedence ) ) {
hql.append( " nulls first" );
}
else {
hql.append( " nulls last" );
}
}
return hql.toString();
}
}
} }

View File

@ -162,7 +162,7 @@ public abstract class AbstractAuditQuery implements AuditQueryImplementor {
orderEntityName, orderEntityName,
orderData.getPropertyName() orderData.getPropertyName()
); );
qb.addOrder( orderEntityAlias, propertyName, orderData.isAscending() ); qb.addOrder( orderEntityAlias, propertyName, orderData.isAscending(), orderData.getNullPrecedence() );
return this; return this;
} }

View File

@ -177,7 +177,7 @@ public class AuditAssociationQueryImpl<Q extends AuditQueryImplementor>
orderEntityName, orderEntityName,
orderData.getPropertyName() orderData.getPropertyName()
); );
queryBuilder.addOrder( orderEntityAlias, propertyName, orderData.isAscending() ); queryBuilder.addOrder( orderEntityAlias, propertyName, orderData.isAscending(), orderData.getNullPrecedence() );
return this; return this;
} }

View File

@ -120,7 +120,7 @@ public class RevisionsOfEntityQuery extends AbstractAuditQuery {
if ( !hasProjection() && !hasOrder ) { if ( !hasProjection() && !hasOrder ) {
String revisionPropertyPath = configuration.getRevisionNumberPath(); String revisionPropertyPath = configuration.getRevisionNumberPath();
qb.addOrder( QueryConstants.REFERENCED_ENTITY_ALIAS, revisionPropertyPath, true ); qb.addOrder( QueryConstants.REFERENCED_ENTITY_ALIAS, revisionPropertyPath, true, null );
} }
if ( !selectEntitiesOnly ) { if ( !selectEntitiesOnly ) {

View File

@ -14,6 +14,14 @@ import org.hibernate.envers.configuration.Configuration;
*/ */
public interface AuditOrder { public interface AuditOrder {
/**
* Specifies the null order precedence for the order-by column specification.
*
* @param nullPrecedence the null precedence, may be {@code null}.
* @return this {@link AuditOrder} for chaining purposes
*/
AuditOrder nulls(NullPrecedence nullPrecedence);
/** /**
* @param configuration the configuration * @param configuration the configuration
* @return the order data. * @return the order data.
@ -25,11 +33,13 @@ public interface AuditOrder {
private final String alias; private final String alias;
private final String propertyName; private final String propertyName;
private final boolean ascending; private final boolean ascending;
private final NullPrecedence nullPrecedence;
public OrderData(String alias, String propertyName, boolean ascending) { public OrderData(String alias, String propertyName, boolean ascending, NullPrecedence nullPrecedence) {
this.alias = alias; this.alias = alias;
this.propertyName = propertyName; this.propertyName = propertyName;
this.ascending = ascending; this.ascending = ascending;
this.nullPrecedence = nullPrecedence;
} }
public String getAlias(String baseAlias) { public String getAlias(String baseAlias) {
@ -44,6 +54,9 @@ public interface AuditOrder {
return ascending; return ascending;
} }
public NullPrecedence getNullPrecedence() {
return nullPrecedence;
}
} }
} }

View File

@ -0,0 +1,24 @@
/*
* 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.query.order;
/**
* Defines the possible null handling modes.
*
* @author Chris Cranford
*/
public enum NullPrecedence {
/**
* Null values will be rendered before non-null values.
*/
FIRST,
/**
* Null values will be rendered after non-null values.
*/
LAST
}

View File

@ -9,6 +9,7 @@ package org.hibernate.envers.query.order.internal;
import org.hibernate.envers.configuration.Configuration; import org.hibernate.envers.configuration.Configuration;
import org.hibernate.envers.query.internal.property.PropertyNameGetter; import org.hibernate.envers.query.internal.property.PropertyNameGetter;
import org.hibernate.envers.query.order.AuditOrder; import org.hibernate.envers.query.order.AuditOrder;
import org.hibernate.envers.query.order.NullPrecedence;
/** /**
* @author Adam Warski (adam at warski dot org) * @author Adam Warski (adam at warski dot org)
@ -18,6 +19,7 @@ public class PropertyAuditOrder implements AuditOrder {
private final String alias; private final String alias;
private final PropertyNameGetter propertyNameGetter; private final PropertyNameGetter propertyNameGetter;
private final boolean asc; private final boolean asc;
private NullPrecedence nullPrecedence;
public PropertyAuditOrder(String alias, PropertyNameGetter propertyNameGetter, boolean asc) { public PropertyAuditOrder(String alias, PropertyNameGetter propertyNameGetter, boolean asc) {
this.alias = alias; this.alias = alias;
@ -25,8 +27,14 @@ public class PropertyAuditOrder implements AuditOrder {
this.asc = asc; this.asc = asc;
} }
@Override
public AuditOrder nulls(NullPrecedence nullPrecedence) {
this.nullPrecedence = nullPrecedence;
return this;
}
@Override @Override
public OrderData getData(Configuration configuration) { public OrderData getData(Configuration configuration) {
return new OrderData( alias, propertyNameGetter.get( configuration ), asc ); return new OrderData( alias, propertyNameGetter.get( configuration ), asc, nullPrecedence );
} }
} }

View File

@ -0,0 +1,78 @@
/*
* 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.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.hibernate.envers.query.AuditEntity;
import org.hibernate.envers.query.order.NullPrecedence;
import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase;
import org.hibernate.orm.test.envers.Priority;
import org.hibernate.orm.test.envers.entities.StrIntTestEntity;
import org.junit.Assert;
import org.junit.Test;
import org.hibernate.testing.TestForIssue;
import org.hibernate.testing.transaction.TransactionUtil;
/**
* Tests for the {@link NullPrecedence} query option on order-bys.
*
* @author Chris Cranford
*/
@TestForIssue( jiraKey = "HHH-14981" )
public class NullPrecedenceTest extends BaseEnversJPAFunctionalTestCase {
Integer id1;
Integer id2;
@Override
protected Class<?>[] getAnnotatedClasses() {
return new Class<?>[] { StrIntTestEntity.class };
}
@Test
@Priority(10)
public void initData() {
// Revision 1
id1 = TransactionUtil.doInJPA(this::entityManagerFactory, entityManager -> {
StrIntTestEntity entity1 = new StrIntTestEntity( null, 1 );
entityManager.persist( entity1 );
return entity1.getId();
} );
// Revision 2
id2 = TransactionUtil.doInJPA( this::entityManagerFactory, entityManager -> {
StrIntTestEntity entity2 = new StrIntTestEntity( "two", 2 );
entityManager.persist( entity2 );
return entity2.getId();
} );
}
@Test
public void testNullPrecedenceFirst() {
List results = getAuditReader().createQuery().forRevisionsOfEntity( StrIntTestEntity.class, true, false )
.addProjection( AuditEntity.property( "number" ) )
.addOrder( AuditEntity.property( "str1" ).asc().nulls( NullPrecedence.FIRST ) )
.getResultList();
List<Integer> expected = new ArrayList<>();
expected.addAll( Arrays.asList( 1, 2 ) );
Assert.assertEquals( expected, results );
}
@Test
public void testNullPrecedenceLast() {
List results = getAuditReader().createQuery().forRevisionsOfEntity( StrIntTestEntity.class, true, false )
.addProjection( AuditEntity.property( "number" ) )
.addOrder( AuditEntity.property( "str1" ).asc().nulls( NullPrecedence.LAST ) )
.getResultList();
List<Integer> expected = new ArrayList<>();
expected.addAll( Arrays.asList( 2, 1 ) );
Assert.assertEquals( expected, results );
}
}