Skip to content

Commit 1fc50dd

Browse files
committed
Skip parameter lookup for unused query parameters.
We now skip binding parameter lookup if the query isn't using named parameters and the parameter is not associated with a name. Also, we check for presence of lookup identifiers to avoid parameter binding that cannot be looked up as they are not used anymore. This can happen when a declared query uses parameters only in the ORDER BY clause that is truncated during count query derivation. Then the query object reports parameters althtough they are not being used. We also refined parameter carryover during count query derivation. Previously, we copied all parameters without introspecting their origin. now, we copy only expression parameters to the derived query as count query derivation doesn't have access to expressions as our query parsers require valid JPQL. Closes #3756
1 parent 9672b30 commit 1fc50dd

File tree

6 files changed

+118
-46
lines changed

6 files changed

+118
-46
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/QueryParameterSetterFactory.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -245,10 +245,13 @@ public QueryParameterSetter create(ParameterBinding binding, DeclaredQuery decla
245245

246246
BindingIdentifier identifier = mia.identifier();
247247

248-
if (declaredQuery.hasNamedParameter()) {
248+
if (declaredQuery.hasNamedParameter() && identifier.hasName()) {
249249
parameter = findParameterForBinding(parameters, identifier.getName());
250-
} else {
250+
} else if (identifier.hasPosition()) {
251251
parameter = findParameterForBinding(parameters, identifier.getPosition() - 1);
252+
} else {
253+
// this can happen when a query uses parameters in ORDER BY and the COUNT query just needs to drop a binding.
254+
parameter = null;
252255
}
253256

254257
return parameter == null //

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/StringQuery.java

+14-6
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,19 @@ public DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection) {
113113
StringQuery stringQuery = new StringQuery(this.queryEnhancer.createCountQueryFor(countQueryProjection), //
114114
this.isNative);
115115

116+
// need to copy expression bindings from the declared to the derived query as JPQL query derivation only sees JPA
117+
// parameter markers and not the original expressions anymore.
116118
if (this.hasParameterBindings() && !this.getParameterBindings().equals(stringQuery.getParameterBindings())) {
117-
stringQuery.getParameterBindings().clear();
118-
stringQuery.getParameterBindings().addAll(this.bindings);
119+
120+
List<ParameterBinding> derivedBindings = stringQuery.getParameterBindings();
121+
122+
for (ParameterBinding binding : bindings) {
123+
124+
if (binding.getOrigin().isExpression() && derivedBindings
125+
.removeIf(it -> !it.getOrigin().isExpression() && it.getIdentifier().equals(binding.getIdentifier()))) {
126+
derivedBindings.add(binding);
127+
}
128+
}
119129
}
120130

121131
return stringQuery;
@@ -235,8 +245,7 @@ private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(St
235245
greatestParameterIndex = 0;
236246
}
237247

238-
SpelExtractor spelExtractor = createSpelExtractor(query, parametersShouldBeAccessedByIndex,
239-
greatestParameterIndex);
248+
SpelExtractor spelExtractor = createSpelExtractor(query, parametersShouldBeAccessedByIndex, greatestParameterIndex);
240249

241250
String resultingQuery = spelExtractor.getQueryString();
242251
Matcher matcher = PARAMETER_BINDING_PATTERN.matcher(resultingQuery);
@@ -340,8 +349,7 @@ private String parseParameterBindingsOfQueryIntoBindingsAndReturnCleanedQuery(St
340349
return resultingQuery;
341350
}
342351

343-
private static SpelExtractor createSpelExtractor(String queryWithSpel, boolean parametersShouldBeAccessedByIndex,
344-
int greatestParameterIndex) {
352+
private static SpelExtractor createSpelExtractor(String queryWithSpel, boolean parametersShouldBeAccessedByIndex, int greatestParameterIndex) {
345353

346354
/*
347355
* If parameters need to be bound by index, we bind the synthetic expression parameters starting from position of the greatest discovered index parameter in order to

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java

+25-11
Original file line numberDiff line numberDiff line change
@@ -3016,22 +3016,36 @@ void handlesColonsFollowedByIntegerInStringLiteral() {
30163016
assertThat(users).extracting(User::getId).containsExactly(expected.getId());
30173017
}
30183018

3019-
@Disabled("ORDER BY CASE appears to be a Hibernate-only feature")
3020-
@Test // DATAJPA-1233
3019+
@Test // DATAJPA-1233, GH-3756
30213020
void handlesCountQueriesWithLessParametersSingleParam() {
3022-
// repository.findAllOrderedBySpecialNameSingleParam("Oliver", PageRequest.of(2, 3));
3021+
3022+
flushTestUsers();
3023+
3024+
Page<User> result = repository.findAllOrderedByNamedParam("Oliver", PageRequest.of(0, 3));
3025+
3026+
assertThat(result.getContent()).containsExactly(firstUser, fourthUser, thirdUser);
3027+
assertThat(result.getTotalElements()).isEqualTo(4);
3028+
3029+
result = repository.findAllOrderedByIndexedParam("Oliver", PageRequest.of(0, 3));
3030+
3031+
assertThat(result.getContent()).containsExactly(firstUser, fourthUser, thirdUser);
3032+
assertThat(result.getTotalElements()).isEqualTo(4);
30233033
}
30243034

3025-
@Disabled("ORDER BY CASE appears to be a Hibernate-only feature")
3026-
@Test // DATAJPA-1233
3035+
@Test // DATAJPA-1233, GH-3756
30273036
void handlesCountQueriesWithLessParametersMoreThanOne() {
3028-
// repository.findAllOrderedBySpecialNameMultipleParams("Oliver", "x", PageRequest.of(2, 3));
3029-
}
30303037

3031-
@Disabled("ORDER BY CASE appears to be a Hibernate-only feature")
3032-
@Test // DATAJPA-1233
3033-
void handlesCountQueriesWithLessParametersMoreThanOneIndexed() {
3034-
// repository.findAllOrderedBySpecialNameMultipleParamsIndexed("x", "Oliver", PageRequest.of(2, 3));
3038+
flushTestUsers();
3039+
3040+
Page<User> result = repository.findAllOrderedBySpecialNameMultipleParams("Oliver", "x", PageRequest.of(0, 3));
3041+
3042+
assertThat(result.getContent()).containsExactly(firstUser, fourthUser, thirdUser);
3043+
assertThat(result.getTotalElements()).isEqualTo(4);
3044+
3045+
result = repository.findAllOrderedBySpecialNameMultipleParamsIndexed("x", "Oliver", PageRequest.of(0, 3));
3046+
3047+
assertThat(result.getContent()).containsExactly(firstUser, fourthUser, thirdUser);
3048+
assertThat(result.getTotalElements()).isEqualTo(4);
30353049
}
30363050

30373051
// DATAJPA-928

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/ExpressionBasedStringQueryUnitTests.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,7 @@ void indexedExpressionsShouldCreateLikeBindings() {
176176
}
177177

178178
@Test
179-
public void doesTemplatingWhenEntityNameSpelIsPresent() {
179+
void doesTemplatingWhenEntityNameSpelIsPresent() {
180180

181181
StringQuery query = new ExpressionBasedStringQuery("select #{#entityName + 'Hallo'} from #{#entityName} u",
182182
metadata, SPEL_PARSER, false);
@@ -185,7 +185,7 @@ public void doesTemplatingWhenEntityNameSpelIsPresent() {
185185
}
186186

187187
@Test
188-
public void doesNoTemplatingWhenEntityNameSpelIsNotPresent() {
188+
void doesNoTemplatingWhenEntityNameSpelIsNotPresent() {
189189

190190
StringQuery query = new ExpressionBasedStringQuery("select #{#entityName + 'Hallo'} from User u", metadata,
191191
SPEL_PARSER, false);
@@ -194,7 +194,7 @@ public void doesNoTemplatingWhenEntityNameSpelIsNotPresent() {
194194
}
195195

196196
@Test
197-
public void doesTemplatingWhenEntityNameSpelIsPresentForBindParameter() {
197+
void doesTemplatingWhenEntityNameSpelIsPresentForBindParameter() {
198198

199199
StringQuery query = new ExpressionBasedStringQuery("select u from #{#entityName} u where name = :#{#something}",
200200
metadata, SPEL_PARSER, false);

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/query/StringQueryUnitTests.java

+50
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,56 @@ void allowsReuseOfParameterWithInAndRegularBinding() {
309309
assertNamedBinding(InParameterBinding.class, "foo_1", bindings.get(1));
310310
}
311311

312+
@Test // GH-3126
313+
void countQueryDerivationRetainsNamedExpressionParameters() {
314+
315+
StringQuery query = new StringQuery(
316+
"select u from User u where foo = :#{bar} ORDER BY CASE WHEN (u.firstname >= :#{name}) THEN 0 ELSE 1 END",
317+
false);
318+
319+
DeclaredQuery countQuery = query.deriveCountQuery(null);
320+
321+
assertThat(countQuery.getParameterBindings()).hasSize(1);
322+
assertThat(countQuery.getParameterBindings()).extracting(ParameterBinding::getOrigin)
323+
.extracting(ParameterOrigin::isExpression).isEqualTo(List.of(true));
324+
325+
query = new StringQuery(
326+
"select u from User u where foo = :#{bar} and bar = :bar ORDER BY CASE WHEN (u.firstname >= :bar) THEN 0 ELSE 1 END",
327+
false);
328+
329+
countQuery = query.deriveCountQuery(null);
330+
331+
assertThat(countQuery.getParameterBindings()).hasSize(2);
332+
assertThat(countQuery.getParameterBindings()) //
333+
.extracting(ParameterBinding::getOrigin) //
334+
.extracting(ParameterOrigin::isExpression).contains(true, false);
335+
}
336+
337+
@Test // GH-3126
338+
void countQueryDerivationRetainsIndexedExpressionParameters() {
339+
340+
StringQuery query = new StringQuery(
341+
"select u from User u where foo = ?#{bar} ORDER BY CASE WHEN (u.firstname >= ?#{name}) THEN 0 ELSE 1 END",
342+
false);
343+
344+
DeclaredQuery countQuery = query.deriveCountQuery(null);
345+
346+
assertThat(countQuery.getParameterBindings()).hasSize(1);
347+
assertThat(countQuery.getParameterBindings()).extracting(ParameterBinding::getOrigin)
348+
.extracting(ParameterOrigin::isExpression).isEqualTo(List.of(true));
349+
350+
query = new StringQuery(
351+
"select u from User u where foo = ?#{bar} and bar = ?1 ORDER BY CASE WHEN (u.firstname >= ?1) THEN 0 ELSE 1 END",
352+
false);
353+
354+
countQuery = query.deriveCountQuery(null);
355+
356+
assertThat(countQuery.getParameterBindings()).hasSize(2);
357+
assertThat(countQuery.getParameterBindings()) //
358+
.extracting(ParameterBinding::getOrigin) //
359+
.extracting(ParameterOrigin::isExpression).contains(true, false);
360+
}
361+
312362
@Test // DATAJPA-461
313363
void detectsMultiplePositionalInParameterBindings() {
314364

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java

+21-24
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,8 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
8282
java.util.Optional<User> findById(Integer primaryKey);
8383

8484
/**
85-
* Redeclaration of {@link CrudRepository#deleteById(java.lang.Object)}. to make sure the transaction
86-
* configuration of the original method is considered if the redeclaration does not carry a {@link Transactional}
87-
* annotation.
85+
* Redeclaration of {@link CrudRepository#deleteById(java.lang.Object)}. to make sure the transaction configuration of
86+
* the original method is considered if the redeclaration does not carry a {@link Transactional} annotation.
8887
*/
8988
@Override
9089
void deleteById(Integer id); // DATACMNS-649
@@ -416,7 +415,8 @@ Window<User> findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S
416415
@Query("select u from User u where u.firstname = ?#{[0]} and u.firstname = ?1 and u.lastname like %?#{[1]}% and u.lastname like %?2%")
417416
List<User> findByFirstnameAndLastnameWithSpelExpression(String firstname, String lastname);
418417

419-
@Query(value = "select * from SD_User", countQuery = "select count(1) from SD_User u where u.lastname = :#{#lastname}", nativeQuery = true)
418+
@Query(value = "select * from SD_User",
419+
countQuery = "select count(1) from SD_User u where u.lastname = :#{#lastname}", nativeQuery = true)
420420
Page<User> findByWithSpelParameterOnlyUsedForCountQuery(String lastname, Pageable page);
421421

422422
// DATAJPA-564
@@ -563,26 +563,23 @@ List<User> findUsersByFirstnameForSpELExpressionWithParameterIndexOnlyWithEntity
563563
@Query("SELECT u FROM User u where u.firstname >= ?1 and u.lastname = '000:1'")
564564
List<User> queryWithIndexedParameterAndColonFollowedByIntegerInString(String firstname);
565565

566-
/**
567-
* TODO: ORDER BY CASE appears to only with Hibernate. The examples attempting to do this through pure JPQL don't
568-
* appear to work with Hibernate, so we must set them aside until we can implement HQL.
569-
*/
570-
// // DATAJPA-1233
571-
// @Query(value = "SELECT u FROM User u ORDER BY CASE WHEN (u.firstname >= :name) THEN 0 ELSE 1 END, u.firstname")
572-
// Page<User> findAllOrderedBySpecialNameSingleParam(@Param("name") String name, Pageable page);
573-
//
574-
// // DATAJPA-1233
575-
// @Query(
576-
// value = "SELECT u FROM User u WHERE :other = 'x' ORDER BY CASE WHEN (u.firstname >= :name) THEN 0 ELSE 1 END,
577-
// u.firstname")
578-
// Page<User> findAllOrderedBySpecialNameMultipleParams(@Param("name") String name, @Param("other") String other,
579-
// Pageable page);
580-
//
581-
// // DATAJPA-1233
582-
// @Query(
583-
// value = "SELECT u FROM User u WHERE ?2 = 'x' ORDER BY CASE WHEN (u.firstname >= ?1) THEN 0 ELSE 1 END,
584-
// u.firstname")
585-
// Page<User> findAllOrderedBySpecialNameMultipleParamsIndexed(String other, String name, Pageable page);
566+
@Query(value = "SELECT u FROM User u ORDER BY CASE WHEN (u.firstname >= :name) THEN 0 ELSE 1 END, u.firstname")
567+
Page<User> findAllOrderedByNamedParam(@Param("name") String name, Pageable page);
568+
569+
@Query(value = "SELECT u FROM User u ORDER BY CASE WHEN (u.firstname >= ?1) THEN 0 ELSE 1 END, u.firstname")
570+
Page<User> findAllOrderedByIndexedParam(String name, Pageable page);
571+
572+
@Query(
573+
value = "SELECT u FROM User u WHERE :other = 'x' ORDER BY CASE WHEN (u.firstname >= :name) THEN 0 ELSE 1 END, u.firstname")
574+
Page<User> findAllOrderedBySpecialNameMultipleParams(@Param("name") String name, @Param("other") String other,
575+
Pageable page);
576+
577+
// Note that parameters used in the order-by statement are just cut off, so we must declare a query that parameter
578+
// label order remains valid even after truncating the order by part. (i.e. WHERE ?2 = 'x' ORDER BY CASE WHEN
579+
// (u.firstname >= ?1) isn't going to work).
580+
@Query(
581+
value = "SELECT u FROM User u WHERE ?1 = 'x' ORDER BY CASE WHEN (u.firstname >= ?2) THEN 0 ELSE 1 END, u.firstname")
582+
Page<User> findAllOrderedBySpecialNameMultipleParamsIndexed(String other, String name, Pageable page);
586583

587584
// DATAJPA-928
588585
Page<User> findByNativeNamedQueryWithPageable(Pageable pageable);

0 commit comments

Comments
 (0)