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 b38449c003..d4abd3084b 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 @@ -743,4 +743,20 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder { @Override JpaOrder desc(Expression x); + + /** + * 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 + */ + JpaOrder 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 + */ + JpaOrder desc(Expression x, boolean nullsFirst); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index 83098726ea..147acbef5a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -63,6 +63,7 @@ import org.hibernate.query.criteria.JpaCoalesce; import org.hibernate.query.criteria.JpaCompoundSelection; import org.hibernate.query.criteria.JpaCriteriaQuery; import org.hibernate.query.criteria.JpaExpression; +import org.hibernate.query.criteria.JpaOrder; import org.hibernate.query.criteria.JpaParameterExpression; import org.hibernate.query.criteria.JpaSelection; import org.hibernate.query.criteria.ValueHandlingMode; @@ -397,6 +398,24 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, return new SqmSortSpecification( (SqmExpression) x, SortOrder.DESCENDING ); } + @Override + public JpaOrder asc(Expression x, boolean nullsFirst) { + return new SqmSortSpecification( + (SqmExpression) x, + SortOrder.ASCENDING, + nullsFirst ? NullPrecedence.FIRST : NullPrecedence.LAST + ); + } + + @Override + public JpaOrder desc(Expression x, boolean nullsFirst) { + return new SqmSortSpecification( + (SqmExpression) x, + SortOrder.DESCENDING, + nullsFirst ? NullPrecedence.FIRST : NullPrecedence.LAST + ); + } + @Override public JpaCompoundSelection tuple(Selection[] selections) { //noinspection unchecked 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..8e8d3c660a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/query/criteria/internal/NullPrecedenceTest.java @@ -0,0 +1,92 @@ +package org.hibernate.query.criteria.internal; + +import java.util.List; + +import org.hibernate.query.criteria.HibernateCriteriaBuilder; + +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; + +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/** + * @author Christian Beikov + */ +@TestForIssue(jiraKey = "HHH-14897") +@Jpa( + annotatedClasses = { NullPrecedenceTest.Foo.class } +) +public class NullPrecedenceTest { + + @Test + public void testNullPrecedence(EntityManagerFactoryScope scope) { + scope.inTransaction( 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(); + assertEquals( 4, resultList.size() ); + assertEquals( 1L, resultList.get( 0 ).getId() ); + assertEquals( 4L, resultList.get( 1 ).getId() ); + assertEquals( 3L, resultList.get( 2 ).getId() ); + 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; + } + } +}