Skip to content

Commit d83bfac

Browse files
committed
Refine count-query derivation parameter post-processing.
We've now expanded parameter post-processing for derived count queries to consider binding types (in, like) and to correctly retain invocation parameter redirects instead of assuming an exact mapping of parameter positions in the final query to the actual invocation argument names/indices. Closes #3784
1 parent ad4317a commit d83bfac

File tree

3 files changed

+122
-2
lines changed

3 files changed

+122
-2
lines changed

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

+14
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ public Object prepare(@Nullable Object valueToBind) {
155155
*/
156156
public boolean bindsTo(ParameterBinding other) {
157157

158+
if (getIdentifier().equals(other.getIdentifier())) {
159+
return true;
160+
}
161+
158162
if (identifier.hasName() && other.identifier.hasName()) {
159163
if (identifier.getName().equals(other.identifier.getName())) {
160164
return true;
@@ -502,6 +506,16 @@ static Expression ofExpression(String expression) {
502506
return new Expression(expression);
503507
}
504508

509+
/**
510+
* Creates a {@link MethodInvocationArgument} object for {@code name}
511+
*
512+
* @param name the parameter name from the method invocation.
513+
* @return {@link MethodInvocationArgument} object for {@code name}.
514+
*/
515+
static MethodInvocationArgument ofParameter(String name) {
516+
return ofParameter(name, null);
517+
}
518+
505519
/**
506520
* Creates a {@link MethodInvocationArgument} object for {@code name} and {@code position}. Either the name or the
507521
* position must be given.

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.function.BiFunction;
2424
import java.util.function.Consumer;
2525
import java.util.function.Function;
26+
import java.util.function.Predicate;
2627
import java.util.regex.Matcher;
2728
import java.util.regex.Pattern;
2829

@@ -121,8 +122,11 @@ public DeclaredQuery deriveCountQuery(@Nullable String countQueryProjection) {
121122

122123
for (ParameterBinding binding : bindings) {
123124

124-
if (binding.getOrigin().isExpression() && derivedBindings
125-
.removeIf(it -> !it.getOrigin().isExpression() && it.getIdentifier().equals(binding.getIdentifier()))) {
125+
Predicate<ParameterBinding> identifier = binding::bindsTo;
126+
Predicate<ParameterBinding> notCompatible = Predicate.not(binding::isCompatibleWith);
127+
128+
// replace incompatible bindings
129+
if (derivedBindings.removeIf(it -> identifier.test(it) && notCompatible.test(it))) {
126130
derivedBindings.add(binding);
127131
}
128132
}

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

+102
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,66 @@ void rewritesNamedLikeToUniqueParametersIfNecessary() {
160160
assertThat(((MethodInvocationArgument) parameterBinding.getOrigin()).identifier().getName()).isEqualTo("firstname");
161161
}
162162

163+
@Test // GH-3784
164+
void rewritesNamedLikeToUniqueParametersRetainingCountQuery() {
165+
166+
DeclaredQuery query = new StringQuery(
167+
"select u from User u where u.firstname like %:firstname or u.firstname like :firstname% or u.firstname = :firstname",
168+
false).deriveCountQuery(null);
169+
170+
assertThat(query.getQueryString()) //
171+
.isEqualTo(
172+
"select count(u) from User u where u.firstname like :firstname or u.firstname like :firstname_1 or u.firstname = :firstname_2");
173+
174+
List<ParameterBinding> bindings = query.getParameterBindings();
175+
assertThat(bindings).hasSize(3);
176+
177+
LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0);
178+
assertThat(binding).isNotNull();
179+
assertThat(binding.getOrigin()).isEqualTo(ParameterOrigin.ofParameter("firstname"));
180+
assertThat(binding.getName()).isEqualTo("firstname");
181+
assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH);
182+
183+
binding = (LikeParameterBinding) bindings.get(1);
184+
assertThat(binding).isNotNull();
185+
assertThat(binding.getOrigin()).isEqualTo(ParameterOrigin.ofParameter("firstname"));
186+
assertThat(binding.getName()).isEqualTo("firstname_1");
187+
assertThat(binding.getType()).isEqualTo(Type.STARTING_WITH);
188+
189+
ParameterBinding parameterBinding = bindings.get(2);
190+
assertThat(parameterBinding).isNotNull();
191+
assertThat(parameterBinding.getOrigin()).isEqualTo(ParameterOrigin.ofParameter("firstname"));
192+
assertThat(parameterBinding.getName()).isEqualTo("firstname_2");
193+
assertThat(((MethodInvocationArgument) parameterBinding.getOrigin()).identifier().getName()).isEqualTo("firstname");
194+
}
195+
196+
@Test // GH-3784
197+
void rewritesExpressionsLikeToUniqueParametersRetainingCountQuery() {
198+
199+
DeclaredQuery query = new StringQuery(
200+
"select u from User u where u.firstname like %:#{firstname} or u.firstname like :#{firstname}%", false)
201+
.deriveCountQuery(null);
202+
203+
assertThat(query.getQueryString()) //
204+
.isEqualTo(
205+
"select count(u) from User u where u.firstname like :__$synthetic$__1 or u.firstname like :__$synthetic$__2");
206+
207+
List<ParameterBinding> bindings = query.getParameterBindings();
208+
assertThat(bindings).hasSize(2);
209+
210+
LikeParameterBinding binding = (LikeParameterBinding) bindings.get(0);
211+
assertThat(binding).isNotNull();
212+
assertThat(binding.getOrigin()).isEqualTo(ParameterOrigin.ofExpression("firstname"));
213+
assertThat(binding.getName()).isEqualTo("__$synthetic$__1");
214+
assertThat(binding.getType()).isEqualTo(Type.ENDING_WITH);
215+
216+
binding = (LikeParameterBinding) bindings.get(1);
217+
assertThat(binding).isNotNull();
218+
assertThat(binding.getOrigin()).isEqualTo(ParameterOrigin.ofExpression("firstname"));
219+
assertThat(binding.getName()).isEqualTo("__$synthetic$__2");
220+
assertThat(binding.getType()).isEqualTo(Type.STARTING_WITH);
221+
}
222+
163223
@Test // GH-3041
164224
void rewritesPositionalLikeToUniqueParametersIfNecessary() {
165225

@@ -263,6 +323,48 @@ void detectsMultipleNamedInParameterBindings() {
263323
assertNamedBinding(ParameterBinding.class, "bar", bindings.get(2));
264324
}
265325

326+
@Test // GH-3784
327+
void deriveCountQueryWithNamedInRetainsOrigin() {
328+
329+
String queryString = "select u from User u where (:logins) IS NULL OR LOWER(u.login) IN (:logins)";
330+
DeclaredQuery query = new StringQuery(queryString, false).deriveCountQuery(null);
331+
332+
assertThat(query.getQueryString())
333+
.isEqualTo("select count(u) from User u where (:logins) IS NULL OR LOWER(u.login) IN (:logins_1)");
334+
335+
List<ParameterBinding> bindings = query.getParameterBindings();
336+
assertThat(bindings).hasSize(2);
337+
338+
assertNamedBinding(ParameterBinding.class, "logins", bindings.get(0));
339+
assertThat((MethodInvocationArgument) bindings.get(0).getOrigin()).extracting(MethodInvocationArgument::identifier)
340+
.extracting(BindingIdentifier::getName).isEqualTo("logins");
341+
342+
assertNamedBinding(InParameterBinding.class, "logins_1", bindings.get(1));
343+
assertThat((MethodInvocationArgument) bindings.get(1).getOrigin()).extracting(MethodInvocationArgument::identifier)
344+
.extracting(BindingIdentifier::getName).isEqualTo("logins");
345+
}
346+
347+
@Test // GH-3784
348+
void deriveCountQueryWithPositionalInRetainsOrigin() {
349+
350+
String queryString = "select u from User u where (?1) IS NULL OR LOWER(u.login) IN (?1)";
351+
DeclaredQuery query = new StringQuery(queryString, false).deriveCountQuery(null);
352+
353+
assertThat(query.getQueryString())
354+
.isEqualTo("select count(u) from User u where (?1) IS NULL OR LOWER(u.login) IN (?2)");
355+
356+
List<ParameterBinding> bindings = query.getParameterBindings();
357+
assertThat(bindings).hasSize(2);
358+
359+
assertPositionalBinding(ParameterBinding.class, 1, bindings.get(0));
360+
assertThat((MethodInvocationArgument) bindings.get(0).getOrigin()).extracting(MethodInvocationArgument::identifier)
361+
.extracting(BindingIdentifier::getPosition).isEqualTo(1);
362+
363+
assertPositionalBinding(InParameterBinding.class, 2, bindings.get(1));
364+
assertThat((MethodInvocationArgument) bindings.get(1).getOrigin()).extracting(MethodInvocationArgument::identifier)
365+
.extracting(BindingIdentifier::getPosition).isEqualTo(1);
366+
}
367+
266368
@Test // DATAJPA-461
267369
void detectsPositionalInParameterBindings() {
268370

0 commit comments

Comments
 (0)