Skip to content

Commit 7d08929

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 8a0b7e2 commit 7d08929

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
@@ -41,6 +41,7 @@
4141
import java.util.Map;
4242
import java.util.Optional;
4343
import java.util.Set;
44+
import java.util.stream.IntStream;
4445
import java.util.stream.Stream;
4546

4647
import org.assertj.core.api.SoftAssertions;
@@ -1743,6 +1744,25 @@ void shouldFindUserByFirstnameAndLastnameWithSpelExpressionInStringBasedQuery()
17431744
assertThat(users).containsOnly(firstUser);
17441745
}
17451746

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

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)
@@ -217,6 +221,23 @@ void createsNativeCountQuery() throws Exception {
217221
verify(em).createNativeQuery(anyString());
218222
}
219223

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

@@ -260,12 +281,21 @@ void resolvesExpressionInCountQuery() throws Exception {
260281
}
261282

262283
private AbstractJpaQuery createJpaQuery(Method method) {
284+
return createJpaQuery(method, null);
285+
}
263286

264-
JpaQueryMethod queryMethod = new JpaQueryMethod(method, metadata, factory, extractor);
265-
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, queryMethod.getAnnotatedQuery(), null,
287+
private AbstractJpaQuery createJpaQuery(JpaQueryMethod queryMethod, @Nullable String queryString, @Nullable String countQueryString) {
288+
289+
return JpaQueryFactory.INSTANCE.fromMethodWithQueryString(queryMethod, em, queryString, countQueryString,
266290
QueryRewriter.IdentityQueryRewriter.INSTANCE, EVALUATION_CONTEXT_PROVIDER);
267291
}
268292

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

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

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

300334
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
@@ -449,6 +449,9 @@ Window<User> findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S
449449
@Query("select u from User u where u.firstname = ?#{[0]} and u.firstname = ?1 and u.lastname like %?#{[1]}% and u.lastname like %?2%")
450450
List<User> findByFirstnameAndLastnameWithSpelExpression(String firstname, String lastname);
451451

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

0 commit comments

Comments
 (0)