From 51d8211687c328e460c8b1a8a24640b31ac402bc Mon Sep 17 00:00:00 2001 From: Ilya Bakaev Date: Fri, 5 Jun 2026 18:30:59 +0200 Subject: [PATCH] Fix null check for querydsl predicate executor Signed-off-by: Ilya Bakaev --- .../support/QuerydslJpaPredicateExecutor.java | 7 +- .../jpa/repository/UserRepositoryTests.java | 125 ++++++++++++++++++ 2 files changed, 129 insertions(+), 3 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java index 114aa0321c..b1570e283a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java @@ -52,7 +52,6 @@ import com.querydsl.core.types.ConstantImpl; import com.querydsl.core.types.EntityPath; import com.querydsl.core.types.Expression; -import com.querydsl.core.types.NullExpression; import com.querydsl.core.types.Ops; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; @@ -74,6 +73,7 @@ * @author Jens Schauder * @author Greg Turnquist * @author Yanming Zhou + * @author Ilya Bakaev */ public class QuerydslJpaPredicateExecutor implements QuerydslPredicateExecutor, JpaRepositoryConfigurationAware { @@ -424,8 +424,9 @@ public BooleanExpression compare(Order order, Expression propertyExpression, @Override public BooleanExpression compare(String property, Expression propertyExpression, @Nullable Object value) { - return Expressions.booleanOperation(Ops.EQ, propertyExpression, - value == null ? NullExpression.DEFAULT : ConstantImpl.create(value)); + return value == null + ? Expressions.booleanOperation(Ops.IS_NULL, propertyExpression) + : Expressions.booleanOperation(Ops.EQ, propertyExpression, ConstantImpl.create(value)); } @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index fc3c80c167..62310f0532 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -104,6 +104,7 @@ * @author Geoffrey Deremetz * @author Krzysztof Krason * @author Yanming Zhou + * @author Ilya Bakaev */ @ExtendWith(SpringExtension.class) @ContextConfiguration("classpath:application-context.xml") @@ -1522,6 +1523,130 @@ void scrollByPartTreeKeysetBackward() { assertThat(previousWindow.hasNext()).isFalse(); } + @Test + void scrollByPredicateKeysetWithAscNullsFirst() { + + User jane1 = new User("Jane", "Doe", "jane@doe1.com"); + User jane2 = new User("Jane", null, "jane@doe2.com"); + User jane3 = new User("Jane", null, "jane@doe3.com"); + User john1 = new User("John", "Doe", "john@doe1.com"); + User john2 = new User("John", null, "john@doe2.com"); + User john3 = new User("John", null, "john@doe3.com"); + + repository.saveAllAndFlush(Arrays.asList(john1, john2, john3, jane1, jane2, jane3)); + + Sort sort = Sort.by( + Order.asc("firstname"), + Order.asc("lastname").nullsFirst(), + Order.asc("emailAddress") + ); + + Window firstWindow = repository.findBy(QUser.user.firstname.startsWith("J"), + q -> q.limit(1).sortBy(sort).scroll(ScrollPosition.keyset())); + + assertThat(firstWindow).containsOnly(jane2); + assertThat(firstWindow.hasNext()).isTrue(); + + Window nextWindow = repository.findBy(QUser.user.firstname.startsWith("J"), + q -> q.limit(3).sortBy(sort).scroll(firstWindow.positionAt(0))); + + assertThat(nextWindow).containsExactly(jane3, jane1, john2); + assertThat(nextWindow.hasNext()).isTrue(); + } + + @Test // GH-2878 + void scrollByPredicateKeysetWithDescNullsFirst() { + + User jane1 = new User("Jane", "Doe", "jane@doe1.com"); + User jane2 = new User("Jane", null, "jane@doe2.com"); + User jane3 = new User("Jane", null, "jane@doe3.com"); + User john1 = new User("John", "Doe", "john@doe1.com"); + User john2 = new User("John", null, "john@doe2.com"); + User john3 = new User("John", null, "john@doe3.com"); + + repository.saveAllAndFlush(Arrays.asList(john1, john2, john3, jane1, jane2, jane3)); + + Sort sort = Sort.by( + Order.asc("firstname"), + Order.desc("lastname").nullsFirst(), + Order.asc("emailAddress") + ); + + Window firstWindow = repository.findBy(QUser.user.firstname.startsWith("J"), + q -> q.limit(1).sortBy(sort).scroll(ScrollPosition.keyset())); + + assertThat(firstWindow).containsOnly(jane2); + assertThat(firstWindow.hasNext()).isTrue(); + + Window nextWindow = repository.findBy(QUser.user.firstname.startsWith("J"), + q -> q.limit(3).sortBy(sort).scroll(firstWindow.positionAt(0))); + + assertThat(nextWindow).containsExactly(jane3, jane1, john2); + assertThat(nextWindow.hasNext()).isTrue(); + } + + @Test // GH-2878 + void scrollByPredicateKeysetWithAscNullsLast() { + + User jane1 = new User("Jane", "Doe", "jane@doe1.com"); + User jane2 = new User("Jane", null, "jane@doe2.com"); + User jane3 = new User("Jane", null, "jane@doe3.com"); + User john1 = new User("John", "Doe", "john@doe1.com"); + User john2 = new User("John", null, "john@doe2.com"); + User john3 = new User("John", null, "john@doe3.com"); + + repository.saveAllAndFlush(Arrays.asList(john1, john2, john3, jane1, jane2, jane3)); + + Sort sort = Sort.by( + Order.asc("firstname"), + Order.asc("lastname").nullsLast(), + Order.asc("emailAddress") + ); + + Window firstWindow = repository.findBy(QUser.user.firstname.startsWith("J"), + q -> q.limit(1).sortBy(sort).scroll(ScrollPosition.keyset())); + + assertThat(firstWindow).containsOnly(jane1); + assertThat(firstWindow.hasNext()).isTrue(); + + Window nextWindow = repository.findBy(QUser.user.firstname.startsWith("J"), + q -> q.limit(3).sortBy(sort).scroll(firstWindow.positionAt(0))); + + assertThat(nextWindow).containsExactly(jane2, jane3, john1); + assertThat(nextWindow.hasNext()).isTrue(); + } + + @Test // GH-2878 + void scrollByPredicateKeysetWithDescNullsLast() { + + User jane1 = new User("Jane", "Doe", "jane@doe1.com"); + User jane2 = new User("Jane", null, "jane@doe2.com"); + User jane3 = new User("Jane", null, "jane@doe3.com"); + User john1 = new User("John", "Doe", "john@doe1.com"); + User john2 = new User("John", null, "john@doe2.com"); + User john3 = new User("John", null, "john@doe3.com"); + + repository.saveAllAndFlush(Arrays.asList(john1, john2, john3, jane1, jane2, jane3)); + + Sort sort = Sort.by( + Order.asc("firstname"), + Order.desc("lastname").nullsLast(), + Order.asc("emailAddress") + ); + + Window firstWindow = repository.findBy(QUser.user.firstname.startsWith("J"), + q -> q.limit(1).sortBy(sort).scroll(ScrollPosition.keyset())); + + assertThat(firstWindow).containsOnly(jane1); + assertThat(firstWindow.hasNext()).isTrue(); + + Window nextWindow = repository.findBy(QUser.user.firstname.startsWith("J"), + q -> q.limit(3).sortBy(sort).scroll(firstWindow.positionAt(0))); + + assertThat(nextWindow).containsExactly(jane2, jane3, john1); + assertThat(nextWindow.hasNext()).isTrue(); + } + @Test // GH-3015, GH-3407 void shouldApplyOffsetScrollPosition() {