HHH-14897 Allow ordering with nulls first/last in JPA Criteria

This commit is contained in:
Christian Beikov 2021-12-06 11:40:47 +01:00
parent 69cd716e37
commit 2d871d64f2
5 changed files with 138 additions and 2 deletions

View File

@ -9,6 +9,7 @@ package org.hibernate.query.criteria;
import java.util.Map; import java.util.Map;
import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.Expression; import javax.persistence.criteria.Expression;
import javax.persistence.criteria.Order;
import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Predicate;
/** /**
@ -67,4 +68,20 @@ public interface HibernateCriteriaBuilder extends CriteriaBuilder {
*/ */
<M extends Map<?,?>> Expression<Integer> mapSize(M map); <M extends Map<?,?>> Expression<Integer> mapSize(M map);
/**
* Create an ordering by the ascending value of the expression.
* @param x expression used to define the ordering
* @param nullsFirst Whether <code>null</code> 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 <code>null</code> should be sorted first
* @return descending ordering corresponding to the expression
*/
Order desc(Expression<?> x, boolean nullsFirst);
} }

View File

@ -250,8 +250,16 @@ public class CriteriaBuilderImpl implements HibernateCriteriaBuilder, Serializab
return new OrderImpl( x, false ); 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<Boolean> expression) { public Predicate wrap(Expression<Boolean> expression) {
if ( Predicate.class.isInstance( expression ) ) { if ( Predicate.class.isInstance( expression ) ) {

View File

@ -395,6 +395,17 @@ public class CriteriaQueryImpl<T> extends AbstractNode implements CriteriaQuery<
jpaqlBuffer.append( sep ) jpaqlBuffer.append( sep )
.append( ( (Renderable) orderSpec.getExpression() ).render( renderingContext ) ) .append( ( (Renderable) orderSpec.getExpression() ).render( renderingContext ) )
.append( orderSpec.isAscending() ? " asc" : " desc" ); .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 = ", "; sep = ", ";
} }
} }

View File

@ -19,14 +19,20 @@ public class OrderImpl implements Order, Serializable {
private final Expression<?> expression; private final Expression<?> expression;
private final boolean ascending; private final boolean ascending;
private final Boolean nullsFirst;
public OrderImpl(Expression<?> expression) { public OrderImpl(Expression<?> expression) {
this( expression, true ); this( expression, true, null );
} }
public OrderImpl(Expression<?> expression, boolean ascending) { public OrderImpl(Expression<?> expression, boolean ascending) {
this(expression, ascending, null);
}
public OrderImpl(Expression<?> expression, boolean ascending, Boolean nullsFirst) {
this.expression = expression; this.expression = expression;
this.ascending = ascending; this.ascending = ascending;
this.nullsFirst = nullsFirst;
} }
public Order reverse() { public Order reverse() {
@ -40,4 +46,8 @@ public class OrderImpl implements Order, Serializable {
public Expression<?> getExpression() { public Expression<?> getExpression() {
return expression; return expression;
} }
public Boolean getNullsFirst() {
return nullsFirst;
}
} }

View File

@ -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<Foo> cq = cb.createQuery( Foo.class );
final Root<Foo> foo = cq.from( Foo.class );
cq.orderBy(
cb.desc( foo.get( "bar" ), true ),
cb.desc( foo.get( "id" ) )
);
final TypedQuery<Foo> tq = entityManager.createQuery( cq );
final List<Foo> 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;
}
}
}