From 1c79fa820f7402c4f763148a771bb985827cceef Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 25 Nov 2024 09:26:55 +0100 Subject: [PATCH] Consider `NULLS` precedence using `Sort` for Criteria Queries. Closes: #3587 Original Pull Request: #3695 --- .../data/jpa/repository/query/QueryUtils.java | 23 ++++++++++++------- .../query/QueryUtilsIntegrationTests.java | 11 +++++---- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java index e625c9460f..302da71f18 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java @@ -29,6 +29,7 @@ import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Nulls; import jakarta.persistence.criteria.Path; import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.Attribute.PersistentAttributeType; @@ -292,9 +293,8 @@ public static String applySorting(String query, Sort sort, @Nullable String alia Set selectionAliases = getFunctionAliases(query); selectionAliases.addAll(getFieldAliases(query)); - String orderClauses = sort.stream() - .map(order -> getOrderClause(joinAliases, selectionAliases, alias, order)) - .collect(Collectors.joining(", ")); + String orderClauses = sort.stream().map(order -> getOrderClause(joinAliases, selectionAliases, alias, order)) + .collect(Collectors.joining(", ")); builder.append(orderClauses); @@ -753,18 +753,25 @@ private static jakarta.persistence.criteria.Order toJpaOrder(Order order, From expression = toExpressionRecursively(from, property); - if (order.getNullHandling() != Sort.NullHandling.NATIVE) { - throw new UnsupportedOperationException("Applying Null Precedence using Criteria Queries is not yet supported."); - } + Nulls nulls = toNulls(order.getNullHandling()); if (order.isIgnoreCase() && String.class.equals(expression.getJavaType())) { Expression upper = cb.lower((Expression) expression); - return order.isAscending() ? cb.asc(upper) : cb.desc(upper); + return order.isAscending() ? cb.asc(upper, nulls) : cb.desc(upper, nulls); } else { - return order.isAscending() ? cb.asc(expression) : cb.desc(expression); + return order.isAscending() ? cb.asc(expression, nulls) : cb.desc(expression, nulls); } } + private static Nulls toNulls(Sort.NullHandling nullHandling) { + + return switch (nullHandling) { + case NULLS_LAST -> Nulls.LAST; + case NULLS_FIRST -> Nulls.FIRST; + case NATIVE -> Nulls.NONE; + }; + } + static Expression toExpressionRecursively(From from, PropertyPath property) { return toExpressionRecursively(from, property, false); } diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java index 59eb5f8667..1c7adc781d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/QueryUtilsIntegrationTests.java @@ -32,6 +32,7 @@ import jakarta.persistence.criteria.From; import jakarta.persistence.criteria.Join; import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Nulls; import jakarta.persistence.criteria.Root; import jakarta.persistence.spi.PersistenceProvider; import jakarta.persistence.spi.PersistenceProviderResolver; @@ -314,8 +315,8 @@ void toOrdersCanSortByJoinColumn() { assertThat(orders).hasSize(1); } - @Test // GH-3529 - void nullPrecedenceThroughCriteriaApiNotYetSupported() { + @Test // GH-3529, GH-3587 + void queryUtilsConsidersNullPrecedence() { CriteriaBuilder builder = em.getCriteriaBuilder(); CriteriaQuery query = builder.createQuery(User.class); @@ -324,8 +325,10 @@ void nullPrecedenceThroughCriteriaApiNotYetSupported() { Sort sort = Sort.by(Sort.Order.desc("manager").nullsFirst()); - assertThatExceptionOfType(UnsupportedOperationException.class) - .isThrownBy(() -> QueryUtils.toOrders(sort, join, builder)); + List orders = QueryUtils.toOrders(sort, join, builder); + for (jakarta.persistence.criteria.Order order : orders) { + assertThat(order.getNullPrecedence()).isEqualTo(Nulls.FIRST); + } } /**