Skip to content

Commit 93385c7

Browse files
christophstroblmp911de
authored andcommitted
Create ParameterBindings for count query instead of reusing query bindings.
This commit makes sure we create individual parameter bindings for the count query instead of reusing the bindings from the actual query. This ensures we do not miss bindings that are present only in the count query. Closes: #3293 Original pull request: #3339
1 parent e5d61e1 commit 93385c7

File tree

5 files changed

+89
-8
lines changed

5 files changed

+89
-8
lines changed

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717

1818
import jakarta.persistence.EntityManager;
1919
import jakarta.persistence.Query;
20-
2120
import org.springframework.data.domain.Pageable;
2221
import org.springframework.data.domain.Sort;
2322
import org.springframework.data.jpa.repository.QueryRewriter;
@@ -28,6 +27,7 @@
2827
import org.springframework.expression.spel.standard.SpelExpressionParser;
2928
import org.springframework.lang.Nullable;
3029
import org.springframework.util.Assert;
30+
import org.springframework.util.StringUtils;
3131

3232
/**
3333
* Base class for {@link String} based JPA queries.
@@ -49,6 +49,7 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery {
4949
private final SpelExpressionParser parser;
5050
private final QueryParameterSetter.QueryMetadataCache metadataCache = new QueryParameterSetter.QueryMetadataCache();
5151
private final QueryRewriter queryRewriter;
52+
private final Lazy<ParameterBinder> countParameterBinder;
5253

5354
/**
5455
* Creates a new {@link AbstractStringBasedJpaQuery} from the given {@link JpaQueryMethod}, {@link EntityManager} and
@@ -78,8 +79,17 @@ public AbstractStringBasedJpaQuery(JpaQueryMethod method, EntityManager em, Stri
7879
method.isNativeQuery());
7980

8081
this.countQuery = Lazy.of(() -> {
81-
DeclaredQuery countQuery = query.deriveCountQuery(countQueryString, method.getCountQueryProjection());
82-
return ExpressionBasedStringQuery.from(countQuery, method.getEntityInformation(), parser, method.isNativeQuery());
82+
83+
if(StringUtils.hasText(countQueryString)) {
84+
85+
return new ExpressionBasedStringQuery(countQueryString, method.getEntityInformation(), parser,
86+
method.isNativeQuery());
87+
}
88+
return query.deriveCountQuery(null, method.getCountQueryProjection());
89+
});
90+
91+
this.countParameterBinder = Lazy.of(() -> {
92+
return this.createCountBinder(this.countQuery.get());
8393
});
8494

8595
this.parser = parser;
@@ -113,6 +123,10 @@ protected ParameterBinder createBinder() {
113123
evaluationContextProvider);
114124
}
115125

126+
protected ParameterBinder createCountBinder(DeclaredQuery countQuery) {
127+
return ParameterBinderFactory.createQueryAwareBinder(getQueryMethod().getParameters(), countQuery, parser, evaluationContextProvider);
128+
}
129+
116130
@Override
117131
protected Query doCreateCountQuery(JpaParametersParameterAccessor accessor) {
118132

@@ -125,7 +139,7 @@ protected Query doCreateCountQuery(JpaParametersParameterAccessor accessor) {
125139

126140
QueryParameterSetter.QueryMetadata metadata = metadataCache.getMetadata(queryString, query);
127141

128-
parameterBinder.get().bind(metadata.withQuery(query), accessor, QueryParameterSetter.ErrorHandling.LENIENT);
142+
countParameterBinder.get().bind(metadata.withQuery(query), accessor, QueryParameterSetter.ErrorHandling.LENIENT);
129143

130144
return query;
131145
}

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,9 +111,19 @@ public List<ParameterBinding> getParameterBindings() {
111111
@Override
112112
public DeclaredQuery deriveCountQuery(@Nullable String countQuery, @Nullable String countQueryProjection) {
113113

114-
return DeclaredQuery.of( //
115-
countQuery != null ? countQuery : this.queryEnhancer.createCountQueryFor(countQueryProjection), //
114+
if(StringUtils.hasText(countQuery)) {
115+
return new StringQuery(countQuery, this.isNative);
116+
}
117+
118+
StringQuery stringQuery = new StringQuery(this.queryEnhancer.createCountQueryFor(countQueryProjection), //
116119
this.isNative);
120+
121+
if(this.hasParameterBindings() && !this.getParameterBindings().equals(stringQuery.getParameterBindings())) {
122+
stringQuery.getParameterBindings().clear();
123+
stringQuery.getParameterBindings().addAll(this.bindings);
124+
}
125+
126+
return stringQuery;
117127
}
118128

119129
@Override

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import java.util.Map;
4343
import java.util.Optional;
4444
import java.util.Set;
45+
import java.util.stream.IntStream;
4546
import java.util.stream.Stream;
4647

4748
import org.assertj.core.api.SoftAssertions;
@@ -1747,6 +1748,25 @@ void shouldFindUserByFirstnameAndLastnameWithSpelExpressionInStringBasedQuery()
17471748
assertThat(users).containsOnly(firstUser);
17481749
}
17491750

1751+
@Test // GH-2393
1752+
void bindsSpELParameterOnlyUsedInCountQuery() {
1753+
1754+
flushTestUsers(); // add some noise
1755+
1756+
IntStream.range(0, 10).mapToObj(counter -> {
1757+
User source = new User();
1758+
source.setFirstname("%d-Spring".formatted(counter));
1759+
source.setLastname("Data");
1760+
source.setEmailAddress("spring-%[email protected]".formatted(counter));
1761+
return source;
1762+
}).forEach(repository::save);
1763+
em.flush();
1764+
1765+
Page<User> users = repository.findByWithSpelParameterOnlyUsedForCountQuery("Data", PageRequest.of(0, 2));
1766+
assertThat(users.getSize()).isEqualTo(2);
1767+
assertThat(users.getTotalElements()).isEqualTo(10);
1768+
}
1769+
17501770
@Test // DATAJPA-564
17511771
void shouldFindUserByLastnameWithSpelExpressionInStringBasedQuery() {
17521772

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

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,12 @@
2828
import java.lang.reflect.Method;
2929
import java.util.Collection;
3030
import java.util.List;
31+
import java.util.Optional;
3132

3233
import org.junit.jupiter.api.BeforeEach;
3334
import org.junit.jupiter.api.Test;
3435
import org.junit.jupiter.api.extension.ExtendWith;
36+
import org.mockito.ArgumentCaptor;
3537
import org.mockito.Mock;
3638
import org.mockito.Mockito;
3739
import org.mockito.junit.jupiter.MockitoExtension;
@@ -53,6 +55,7 @@
5355
import org.springframework.data.repository.query.RepositoryQuery;
5456
import org.springframework.data.util.TypeInformation;
5557
import org.springframework.expression.spel.standard.SpelExpressionParser;
58+
import org.springframework.lang.Nullable;
5659

5760
/**
5861
* Unit test for {@link SimpleJpaQuery}.
@@ -65,6 +68,7 @@
6568
* @author Greg Turnquist
6669
* @author Krzysztof Krason
6770
* @author Erik Pellizzon
71+
* @author Christoph Strobl
6872
*/
6973
@ExtendWith(MockitoExtension.class)
7074
@MockitoSettings(strictness = Strictness.LENIENT)
@@ -215,6 +219,23 @@ void createsNativeCountQuery() throws Exception {
215219
verify(em).createNativeQuery(anyString());
216220
}
217221

222+
@Test // GH-3293
223+
void allowsCountQueryUsingParametersNotInOriginalQuery() throws Exception {
224+
225+
when(em.createNativeQuery(anyString())).thenReturn(query);
226+
227+
AbstractJpaQuery jpaQuery = createJpaQuery(
228+
SampleRepository.class.getMethod("findAllWithBindingsOnlyInCountQuery", String.class, Pageable.class), Optional.empty());
229+
230+
jpaQuery.doCreateCountQuery(new JpaParametersParameterAccessor(jpaQuery.getQueryMethod().getParameters(),
231+
new Object[]{"data", PageRequest.of(0, 10)}));
232+
233+
ArgumentCaptor<String> queryStringCaptor = ArgumentCaptor.forClass(String.class);
234+
verify(em).createQuery(queryStringCaptor.capture(), eq(Long.class));
235+
236+
assertThat(queryStringCaptor.getValue()).startsWith("select count(u.id) from User u where u.name =");
237+
}
238+
218239
@Test // DATAJPA-885
219240
void projectsWithManuallyDeclaredQuery() throws Exception {
220241

@@ -258,12 +279,21 @@ void resolvesExpressionInCountQuery() throws Exception {
258279
}
259280

260281
private AbstractJpaQuery createJpaQuery(Method method) {
282+
return createJpaQuery(method, null);
283+
}
261284

262-
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
263-
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, queryMethod.getAnnotatedQuery(), null,
285+
private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString, @Nullable String countQueryString) {
286+
287+
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, queryString, countQueryString,
264288
QueryRewriter.IdentityQueryRewriter.INSTANCE, EVALUATION_CONTEXT_PROVIDER);
265289
}
266290

291+
private AbstractJpaQuery createJpaQuery(Method method, @Nullable Optional<String> countQueryString) {
292+
293+
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
294+
return createJpaQuery(queryMethod, queryMethod.getAnnotatedQuery(), countQueryString == null ? null : countQueryString.orElse(queryMethod.getCountQuery()));
295+
}
296+
267297
interface SampleRepository {
268298

269299
@Query(value = "SELECT u FROM User u WHERE u.lastname = ?1", nativeQuery = true)
@@ -293,6 +323,10 @@ interface SampleRepository {
293323
@Query(value = "select u from #{#entityName} u", countQuery = "select count(u.id) from #{#entityName} u")
294324
List<User> findAllWithExpressionInCountQuery(Pageable pageable);
295325

326+
327+
@Query(value = "select u from User u", countQuery = "select count(u.id) from #{#entityName} u where u.name = :#{#arg0}")
328+
List<User> findAllWithBindingsOnlyInCountQuery(String arg0, Pageable pageable);
329+
296330
}
297331

298332
interface UserProjection {}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,9 @@ Window<User> findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S
446446
@Query("select u from User u where u.firstname = ?#{[0]} and u.firstname = ?1 and u.lastname like %?#{[1]}% and u.lastname like %?2%")
447447
List<User> findByFirstnameAndLastnameWithSpelExpression(String firstname, String lastname);
448448

449+
@Query(value = "select * from SD_User", countQuery = "select count(1) from SD_User u where u.lastname = :#{#lastname}", nativeQuery = true)
450+
Page<User> findByWithSpelParameterOnlyUsedForCountQuery(String lastname, Pageable page);
451+
449452
// DATAJPA-564
450453
@Query("select u from User u where u.lastname like %:#{[0]}% and u.lastname like %:lastname%")
451454
List<User> findByLastnameWithSpelExpression(@Param("lastname") String lastname);

0 commit comments

Comments
 (0)