From 25421733d665f225f7b3c8f5ae78dd946e4ddf05 Mon Sep 17 00:00:00 2001 From: Chris Cranford Date: Thu, 16 Dec 2021 05:06:33 -0500 Subject: [PATCH] HHH-14981 Support null precedence with Envers Query API --- .../internal/tools/query/QueryBuilder.java | 42 ++++++++-- .../internal/impl/AbstractAuditQuery.java | 2 +- .../impl/AuditAssociationQueryImpl.java | 2 +- .../internal/impl/RevisionsOfEntityQuery.java | 2 +- .../envers/query/order/AuditOrder.java | 15 +++- .../envers/query/order/NullPrecedence.java | 24 ++++++ .../order/internal/PropertyAuditOrder.java | 10 ++- .../integration/query/NullPrecedenceTest.java | 78 +++++++++++++++++++ 8 files changed, 163 insertions(+), 12 deletions(-) create mode 100644 hibernate-envers/src/main/java/org/hibernate/envers/query/order/NullPrecedence.java create mode 100644 hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/query/NullPrecedenceTest.java diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/query/QueryBuilder.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/query/QueryBuilder.java index b56c52ed82..f80def0857 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/query/QueryBuilder.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/query/QueryBuilder.java @@ -30,6 +30,7 @@ import org.hibernate.envers.internal.tools.Triple; import org.hibernate.envers.query.criteria.AuditFunction; import org.hibernate.envers.query.criteria.AuditId; import org.hibernate.envers.query.criteria.AuditProperty; +import org.hibernate.envers.query.order.NullPrecedence; import org.hibernate.envers.tools.Pair; import org.hibernate.query.Query; import org.hibernate.query.internal.QueryLiteralHelper; @@ -63,9 +64,9 @@ public class QueryBuilder { */ private final List froms; /** - * A list of triples (alias, property name, order ascending?). + * A list of order by clauses. */ - private final List> orders; + private final List orders; /** * 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; } - public void addOrder(String alias, String propertyName, boolean ascending) { - orders.add( Triple.make( alias, propertyName, ascending ) ); + public void addOrder(String alias, String propertyName, boolean ascending, NullPrecedence nullPrecedence) { + orders.add( new OrderByClause( alias, propertyName, ascending, nullPrecedence ) ); } public void addOrderFragment(String alias, String orderByCollectionRole) { @@ -382,10 +383,9 @@ public class QueryBuilder { private List getOrderList() { final List orderList = new ArrayList<>(); - for ( Triple order : orders ) { - orderList.add( order.getFirst() + "." + order.getSecond() + " " + (order.getThird() ? "asc" : "desc") ); + for ( OrderByClause orderByClause : orders ) { + orderList.add( orderByClause.renderToHql() ); } - 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(); + } + } } 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 45050d0516..212fa5e07d 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 @@ -162,7 +162,7 @@ public abstract class AbstractAuditQuery implements AuditQueryImplementor { orderEntityName, orderData.getPropertyName() ); - qb.addOrder( orderEntityAlias, propertyName, orderData.isAscending() ); + qb.addOrder( orderEntityAlias, propertyName, orderData.isAscending(), orderData.getNullPrecedence() ); return this; } 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/AuditAssociationQueryImpl.java index f6dae9c5c0..75d97b021d 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/AuditAssociationQueryImpl.java @@ -177,7 +177,7 @@ public class AuditAssociationQueryImpl orderEntityName, orderData.getPropertyName() ); - queryBuilder.addOrder( orderEntityAlias, propertyName, orderData.isAscending() ); + queryBuilder.addOrder( orderEntityAlias, propertyName, orderData.isAscending(), orderData.getNullPrecedence() ); return this; } 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 afbaa342a8..6d3b78a6f4 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 @@ -120,7 +120,7 @@ public class RevisionsOfEntityQuery extends AbstractAuditQuery { if ( !hasProjection() && !hasOrder ) { String revisionPropertyPath = configuration.getRevisionNumberPath(); - qb.addOrder( QueryConstants.REFERENCED_ENTITY_ALIAS, revisionPropertyPath, true ); + qb.addOrder( QueryConstants.REFERENCED_ENTITY_ALIAS, revisionPropertyPath, true, null ); } if ( !selectEntitiesOnly ) { diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/query/order/AuditOrder.java b/hibernate-envers/src/main/java/org/hibernate/envers/query/order/AuditOrder.java index fa088d5cf5..3e59a61e70 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/query/order/AuditOrder.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/query/order/AuditOrder.java @@ -14,6 +14,14 @@ import org.hibernate.envers.configuration.Configuration; */ 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 * @return the order data. @@ -25,11 +33,13 @@ public interface AuditOrder { private final String alias; private final String propertyName; 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.propertyName = propertyName; this.ascending = ascending; + this.nullPrecedence = nullPrecedence; } public String getAlias(String baseAlias) { @@ -44,6 +54,9 @@ public interface AuditOrder { return ascending; } + public NullPrecedence getNullPrecedence() { + return nullPrecedence; + } } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/query/order/NullPrecedence.java b/hibernate-envers/src/main/java/org/hibernate/envers/query/order/NullPrecedence.java new file mode 100644 index 0000000000..8919792d27 --- /dev/null +++ b/hibernate-envers/src/main/java/org/hibernate/envers/query/order/NullPrecedence.java @@ -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 . + */ +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 +} diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/query/order/internal/PropertyAuditOrder.java b/hibernate-envers/src/main/java/org/hibernate/envers/query/order/internal/PropertyAuditOrder.java index 3a93a2c27d..6803a824fd 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/query/order/internal/PropertyAuditOrder.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/query/order/internal/PropertyAuditOrder.java @@ -9,6 +9,7 @@ package org.hibernate.envers.query.order.internal; import org.hibernate.envers.configuration.Configuration; import org.hibernate.envers.query.internal.property.PropertyNameGetter; import org.hibernate.envers.query.order.AuditOrder; +import org.hibernate.envers.query.order.NullPrecedence; /** * @author Adam Warski (adam at warski dot org) @@ -18,6 +19,7 @@ public class PropertyAuditOrder implements AuditOrder { private final String alias; private final PropertyNameGetter propertyNameGetter; private final boolean asc; + private NullPrecedence nullPrecedence; public PropertyAuditOrder(String alias, PropertyNameGetter propertyNameGetter, boolean asc) { this.alias = alias; @@ -25,8 +27,14 @@ public class PropertyAuditOrder implements AuditOrder { this.asc = asc; } + @Override + public AuditOrder nulls(NullPrecedence nullPrecedence) { + this.nullPrecedence = nullPrecedence; + return this; + } + @Override public OrderData getData(Configuration configuration) { - return new OrderData( alias, propertyNameGetter.get( configuration ), asc ); + return new OrderData( alias, propertyNameGetter.get( configuration ), asc, nullPrecedence ); } } diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/query/NullPrecedenceTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/query/NullPrecedenceTest.java new file mode 100644 index 0000000000..c323fb8e7b --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/query/NullPrecedenceTest.java @@ -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 . + */ +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 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 expected = new ArrayList<>(); + expected.addAll( Arrays.asList( 2, 1 ) ); + Assert.assertEquals( expected, results ); + } +}