From 2d871d64f272d99a1da806a7328fef4f2a09f91f Mon Sep 17 00:00:00 2001 From: Christian Beikov Date: Mon, 6 Dec 2021 11:40:47 +0100 Subject: [PATCH] HHH-14897 Allow ordering with nulls first/last in JPA Criteria --- .../criteria/HibernateCriteriaBuilder.java | 17 ++++ .../internal/CriteriaBuilderImpl.java | 10 ++- .../criteria/internal/CriteriaQueryImpl.java | 11 +++ .../query/criteria/internal/OrderImpl.java | 12 ++- .../criteria/internal/NullPrecedenceTest.java | 90 +++++++++++++++++++ 5 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 hibernate-core/src/test/java/org/hibernate/query/criteria/internal/NullPrecedenceTest.java diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java index ac787039b1..a4784a9f8c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/HibernateCriteriaBuilder.java @@ -9,6 +9,7 @@ package org.hibernate.query.criteria; import java.util.Map; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.Expression; +import javax.persistence.criteria.Order; import javax.persistence.criteria.Predicate; /** @@ -67,4 +68,20 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { */ > Expression mapSize(M map); + /** + * Create an ordering by the ascending value of the expression. + * @param x expression used to define the ordering + * @param nullsFirst Whether null should be sorted first + * @return ascending ordering corresponding to the expression + */ + Order asc(Expression x, boolean nullsFirst); + + /** + * Create an ordering by the descending value of the expression. + * @param x expression used to define the ordering + * @param nullsFirst Whether null should be sorted first + * @return descending ordering corresponding to the expression + */ + Order desc(Expression x, boolean nullsFirst); + } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/CriteriaBuilderImpl.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/CriteriaBuilderImpl.java index 79e049a6ae..82e48fdf7a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/CriteriaBuilderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/CriteriaBuilderImpl.java @@ -250,8 +250,16 @@ public class CriteriaBuilderImpl implements HibernateCriteriaBuilder, Serializab return new OrderImpl( x, false ); } + @Override + public Order asc(Expression x, boolean nullsFirst) { + return new OrderImpl( x, true, nullsFirst ); + } - // predicates ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + @Override + public Order desc(Expression x, boolean nullsFirst) { + return new OrderImpl( x, false, nullsFirst ); + } +// predicates ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ public Predicate wrap(Expression expression) { if ( Predicate.class.isInstance( expression ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/CriteriaQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/CriteriaQueryImpl.java index 6ec5ddd1d6..84ebb81c40 100755 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/CriteriaQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/CriteriaQueryImpl.java @@ -395,6 +395,17 @@ public class CriteriaQueryImpl extends AbstractNode implements CriteriaQuery< jpaqlBuffer.append( sep ) .append( ( (Renderable) orderSpec.getExpression() ).render( renderingContext ) ) .append( orderSpec.isAscending() ? " asc" : " desc" ); + if ( orderSpec instanceof OrderImpl ) { + Boolean nullsFirst = ( (OrderImpl) orderSpec ).getNullsFirst(); + if ( nullsFirst != null ) { + if ( nullsFirst ) { + jpaqlBuffer.append( " nulls first" ); + } + else { + jpaqlBuffer.append( " nulls last" ); + } + } + } sep = ", "; } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/OrderImpl.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/OrderImpl.java index 2078da4166..ca12796a50 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/OrderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/OrderImpl.java @@ -19,14 +19,20 @@ public class OrderImpl implements Order, Serializable { private final Expression expression; private final boolean ascending; + private final Boolean nullsFirst; public OrderImpl(Expression expression) { - this( expression, true ); + this( expression, true, null ); } public OrderImpl(Expression expression, boolean ascending) { + this(expression, ascending, null); + } + + public OrderImpl(Expression expression, boolean ascending, Boolean nullsFirst) { this.expression = expression; this.ascending = ascending; + this.nullsFirst = nullsFirst; } public Order reverse() { @@ -40,4 +46,8 @@ public class OrderImpl implements Order, Serializable { public Expression getExpression() { return expression; } + + public Boolean getNullsFirst() { + return nullsFirst; + } } diff --git a/hibernate-core/src/test/java/org/hibernate/query/criteria/internal/NullPrecedenceTest.java b/hibernate-core/src/test/java/org/hibernate/query/criteria/internal/NullPrecedenceTest.java new file mode 100644 index 0000000000..7e51787666 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/query/criteria/internal/NullPrecedenceTest.java @@ -0,0 +1,90 @@ +package org.hibernate.query.criteria.internal; + +import java.util.List; +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.TypedQuery; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Root; + +import org.hibernate.jpa.test.BaseEntityManagerFunctionalTestCase; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; + +import org.hibernate.testing.TestForIssue; +import org.junit.Assert; +import org.junit.Test; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; + +/** + * @author Christian Beikov + */ +@TestForIssue( jiraKey = "HHH-14897" ) +public class NullPrecedenceTest extends BaseEntityManagerFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { Foo.class }; + } + + @Test + public void testNullPrecedence() { + doInJPA( this::entityManagerFactory, entityManager -> { + entityManager.persist( new Foo( 1L, null ) ); + entityManager.persist( new Foo( 2L, "ABC" ) ); + entityManager.persist( new Foo( 3L, "DEF" ) ); + entityManager.persist( new Foo( 4L, "DEF" ) ); + final HibernateCriteriaBuilder cb = (HibernateCriteriaBuilder) entityManager.getCriteriaBuilder(); + + final CriteriaQuery cq = cb.createQuery( Foo.class ); + final Root foo = cq.from( Foo.class ); + + cq.orderBy( + cb.desc( foo.get( "bar" ), true ), + cb.desc( foo.get( "id" ) ) + ); + + final TypedQuery tq = entityManager.createQuery( cq ); + + final List resultList = tq.getResultList(); + Assert.assertEquals( 4, resultList.size() ); + Assert.assertEquals( 1L, resultList.get( 0 ).getId() ); + Assert.assertEquals( 4L, resultList.get( 1 ).getId() ); + Assert.assertEquals( 3L, resultList.get( 2 ).getId() ); + Assert.assertEquals( 2L, resultList.get( 3 ).getId() ); + } ); + } + + @Entity(name = "Foo") + public static class Foo { + + private long id; + private String bar; + + public Foo() { + } + + public Foo(long id, String bar) { + this.id = id; + this.bar = bar; + } + + @Id + @Column(nullable = false) + public long getId() { + return this.id; + } + public void setId(final long id) { + this.id = id; + } + + public String getBar() { + return bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + } +}