Skip to content

Commit 5312731

Browse files
committed
Reject Limit parameter using String-based queries.
Document limitations, add test cases for derived queries. Closes #1654
1 parent 4ceb982 commit 5312731

File tree

8 files changed

+124
-24
lines changed

8 files changed

+124
-24
lines changed

spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,11 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera
117117
throw new UnsupportedOperationException(
118118
"Page queries are not supported using string-based queries; Offending method: " + queryMethod);
119119
}
120+
121+
if (queryMethod.getParameters().hasLimitParameter()) {
122+
throw new UnsupportedOperationException(
123+
"Queries with Limit are not supported using string-based queries; Offending method: " + queryMethod);
124+
}
120125
}
121126

122127
@Override

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import org.springframework.data.annotation.Id;
5353
import org.springframework.data.domain.Example;
5454
import org.springframework.data.domain.ExampleMatcher;
55+
import org.springframework.data.domain.Limit;
5556
import org.springframework.data.domain.Page;
5657
import org.springframework.data.domain.PageRequest;
5758
import org.springframework.data.domain.Pageable;
@@ -491,6 +492,18 @@ public void pageByNameShouldReturnCorrectResult() {
491492
assertThat(repository.findPageByNameContains("a", PageRequest.of(1, 2)).getContent()).hasSize(1);
492493
}
493494

495+
@Test // GH-1654
496+
public void selectWithLimitShouldReturnCorrectResult() {
497+
498+
repository.saveAll(Arrays.asList(new DummyEntity("a1"), new DummyEntity("a2"), new DummyEntity("a3")));
499+
500+
List<DummyEntity> page = repository.findByNameContains("a", Limit.of(3));
501+
assertThat(page).hasSize(3);
502+
503+
assertThat(repository.findByNameContains("a", Limit.of(2))).hasSize(2);
504+
assertThat(repository.findByNameContains("a", Limit.unlimited())).hasSize(3);
505+
}
506+
494507
@Test // GH-774
495508
public void sliceByNameShouldReturnCorrectResult() {
496509

@@ -1385,6 +1398,8 @@ interface DummyEntityRepository extends CrudRepository<DummyEntity, Long>, Query
13851398

13861399
Page<DummyEntity> findPageByNameContains(String name, Pageable pageable);
13871400

1401+
List<DummyEntity> findByNameContains(String name, Limit limit);
1402+
13881403
Page<DummyProjection> findPageProjectionByName(String name, Pageable pageable);
13891404

13901405
Slice<DummyEntity> findSliceByNameContains(String name, Pageable pageable);

spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import org.springframework.dao.DataAccessException;
3737
import org.springframework.data.convert.ReadingConverter;
3838
import org.springframework.data.convert.WritingConverter;
39+
import org.springframework.data.domain.Limit;
3940
import org.springframework.data.domain.Page;
4041
import org.springframework.data.domain.Pageable;
4142
import org.springframework.data.domain.Slice;
@@ -51,6 +52,7 @@
5152
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
5253
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
5354
import org.springframework.data.repository.query.ExtensionAwareQueryMethodEvaluationContextProvider;
55+
import org.springframework.data.repository.query.Param;
5456
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
5557
import org.springframework.data.spel.spi.EvaluationContextExtension;
5658
import org.springframework.jdbc.core.ResultSetExtractor;
@@ -191,6 +193,16 @@ void pageQueryNotSupported() {
191193
.hasMessageContaining("Page queries are not supported using string-based queries");
192194
}
193195

196+
@Test // GH-1654
197+
void limitNotSupported() {
198+
199+
JdbcQueryMethod queryMethod = createMethod("unsupportedLimitQuery", String.class, Limit.class);
200+
201+
assertThatThrownBy(
202+
() -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter, evaluationContextProvider))
203+
.isInstanceOf(UnsupportedOperationException.class);
204+
}
205+
194206
@Test // GH-1212
195207
void convertsEnumCollectionParameterIntoStringCollectionParameter() {
196208

@@ -230,7 +242,6 @@ void appliesConverterToIterable() {
230242
.extractParameterSource();
231243

232244
assertThat(sqlParameterSource.getValue("value")).isEqualTo("one");
233-
234245
}
235246

236247
QueryFixture forMethod(String name, Class... paramTypes) {
@@ -337,6 +348,9 @@ interface MyRepository extends Repository<Object, Long> {
337348

338349
@Query("SELECT * FROM table WHERE c = :#{myext.testValue} AND c2 = :#{myext.doSomething()}")
339350
Object findBySpelExpression(Object object);
351+
352+
@Query("SELECT * FROM person WHERE lastname = $1")
353+
Object unsupportedLimitQuery(@Param("lastname") String lastname, Limit limit);
340354
}
341355

342356
@Test // GH-619

spring-data-r2dbc/src/main/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQuery.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,21 @@ public StringBasedR2dbcQuery(String query, R2dbcQueryMethod method, R2dbcEntityO
100100
this.expressionQuery = ExpressionQuery.create(query);
101101
this.binder = new ExpressionEvaluatingParameterBinder(expressionQuery, dataAccessStrategy);
102102
this.expressionDependencies = createExpressionDependencies();
103+
104+
if (method.isSliceQuery()) {
105+
throw new UnsupportedOperationException(
106+
"Slice queries are not supported using string-based queries; Offending method: " + method);
107+
}
108+
109+
if (method.isPageQuery()) {
110+
throw new UnsupportedOperationException(
111+
"Page queries are not supported using string-based queries; Offending method: " + method);
112+
}
113+
114+
if (method.getParameters().hasLimitParameter()) {
115+
throw new UnsupportedOperationException(
116+
"Queries with Limit are not supported using string-based queries; Offending method: " + method);
117+
}
103118
}
104119

105120
private ExpressionDependencies createExpressionDependencies() {

spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/AbstractR2dbcRepositoryIntegrationTests.java

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,29 @@
1515
*/
1616
package org.springframework.data.r2dbc.repository;
1717

18+
import static org.assertj.core.api.Assertions.*;
19+
1820
import io.r2dbc.spi.ConnectionFactory;
21+
import reactor.core.publisher.Flux;
22+
import reactor.core.publisher.Hooks;
23+
import reactor.core.publisher.Mono;
24+
import reactor.test.StepVerifier;
25+
26+
import java.util.Arrays;
27+
import java.util.Map;
28+
import java.util.Objects;
29+
import java.util.stream.IntStream;
30+
31+
import javax.sql.DataSource;
32+
1933
import org.assertj.core.api.Condition;
2034
import org.junit.jupiter.api.BeforeEach;
2135
import org.junit.jupiter.api.Test;
2236
import org.springframework.beans.factory.annotation.Autowired;
2337
import org.springframework.dao.DataAccessException;
2438
import org.springframework.data.annotation.Id;
2539
import org.springframework.data.annotation.PersistenceCreator;
40+
import org.springframework.data.domain.Limit;
2641
import org.springframework.data.domain.PageRequest;
2742
import org.springframework.data.domain.Pageable;
2843
import org.springframework.data.r2dbc.repository.support.R2dbcRepositoryFactory;
@@ -35,17 +50,6 @@
3550
import org.springframework.jdbc.core.JdbcTemplate;
3651
import org.springframework.r2dbc.connection.R2dbcTransactionManager;
3752
import org.springframework.transaction.reactive.TransactionalOperator;
38-
import reactor.core.publisher.Flux;
39-
import reactor.core.publisher.Hooks;
40-
import reactor.core.publisher.Mono;
41-
import reactor.test.StepVerifier;
42-
43-
import javax.sql.DataSource;
44-
import java.util.Arrays;
45-
import java.util.Map;
46-
import java.util.stream.IntStream;
47-
48-
import static org.assertj.core.api.Assertions.*;
4953

5054
/**
5155
* Abstract base class for integration tests for {@link LegoSetRepository} using {@link R2dbcRepositoryFactory}.
@@ -143,6 +147,22 @@ void shouldFindItemsByNameContains() {
143147
}).verifyComplete();
144148
}
145149

150+
@Test // GH-1654
151+
void shouldFindItemsByNameContainsWithLimit() {
152+
153+
shouldInsertNewItems();
154+
155+
repository.findByNameContains("F", Limit.of(1)) //
156+
.as(StepVerifier::create) //
157+
.expectNextCount(1) //
158+
.verifyComplete();
159+
160+
repository.findByNameContains("F", Limit.unlimited()) //
161+
.as(StepVerifier::create) //
162+
.expectNextCount(2) //
163+
.verifyComplete();
164+
}
165+
146166
@Test // GH-475, GH-607
147167
void shouldFindApplyingInterfaceProjection() {
148168

@@ -423,6 +443,8 @@ interface LegoSetRepository extends ReactiveCrudRepository<LegoSet, Integer> {
423443

424444
Flux<LegoSet> findByNameContains(String name);
425445

446+
Flux<LegoSet> findByNameContains(String name, Limit limit);
447+
426448
Flux<LegoSet> findFirst10By();
427449

428450
Flux<LegoSet> findAllByOrderByManual(Pageable pageable);
@@ -483,6 +505,7 @@ public static class LegoSet extends Lego implements Buildable {
483505
public LegoSet() {
484506
}
485507

508+
@Override
486509
public String getName() {
487510
return this.name;
488511
}
@@ -547,15 +570,15 @@ public String getUnknown() {
547570

548571
public boolean equals(final Object o) {
549572
if (o == this) return true;
550-
if (!(o instanceof LegoDto)) return false;
551-
final LegoDto other = (LegoDto) o;
573+
if (!(o instanceof final LegoDto other))
574+
return false;
552575
final Object this$name = this.getName();
553576
final Object other$name = other.getName();
554-
if (this$name == null ? other$name != null : !this$name.equals(other$name)) return false;
577+
if (!Objects.equals(this$name, other$name))
578+
return false;
555579
final Object this$unknown = this.getUnknown();
556580
final Object other$unknown = other.getUnknown();
557-
if (this$unknown == null ? other$unknown != null : !this$unknown.equals(other$unknown)) return false;
558-
return true;
581+
return Objects.equals(this$unknown, other$unknown);
559582
}
560583

561584
public int hashCode() {

spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/repository/query/StringBasedR2dbcQueryUnitTests.java

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,8 @@
1818
import static org.assertj.core.api.Assertions.*;
1919
import static org.mockito.Mockito.*;
2020

21-
import io.r2dbc.spi.R2dbcType;
22-
import io.r2dbc.spi.test.MockColumnMetadata;
2321
import io.r2dbc.spi.test.MockResult;
2422
import io.r2dbc.spi.test.MockRow;
25-
import io.r2dbc.spi.test.MockRowMetadata;
2623
import reactor.core.publisher.Flux;
2724
import reactor.test.StepVerifier;
2825

@@ -36,7 +33,7 @@
3633
import org.mockito.junit.jupiter.MockitoExtension;
3734
import org.mockito.junit.jupiter.MockitoSettings;
3835
import org.mockito.quality.Strictness;
39-
36+
import org.springframework.data.domain.Limit;
4037
import org.springframework.data.domain.Sort;
4138
import org.springframework.data.projection.ProjectionFactory;
4239
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
@@ -288,8 +285,6 @@ void usesDtoTypeForDtoResultMapping() {
288285
@Test // gh-612
289286
void selectsSimpleType() {
290287

291-
MockRowMetadata metadata = MockRowMetadata.builder()
292-
.columnMetadata(MockColumnMetadata.builder().name("date").type(R2dbcType.DATE).build()).build();
293288
LocalDate value = LocalDate.now();
294289
MockResult result = MockResult.builder()
295290
.row(MockRow.builder().identified(0, LocalDate.class, value).build()).build();
@@ -309,6 +304,12 @@ void selectsSimpleType() {
309304
flux.as(StepVerifier::create).expectNext(value).verifyComplete();
310305
}
311306

307+
@Test // GH-1654
308+
void rejectsStringBasedLimitQuery() {
309+
assertThatExceptionOfType(UnsupportedOperationException.class)
310+
.isThrownBy(() -> getQueryMethod("unsupportedLimitQuery", String.class, Limit.class));
311+
}
312+
312313
private StringBasedR2dbcQuery getQueryMethod(String name, Class<?>... args) {
313314

314315
Method method = ReflectionUtils.findMethod(SampleRepository.class, name, args);
@@ -366,6 +367,9 @@ private interface SampleRepository extends Repository<Person, String> {
366367

367368
@Query("SELECT MAX(DATE)")
368369
Flux<LocalDate> findAllLocalDates();
370+
371+
@Query("SELECT * FROM person WHERE lastname = $1")
372+
Person unsupportedLimitQuery(@Param("lastname") String lastname, Limit limit);
369373
}
370374

371375
static class PersonDto {
@@ -388,6 +392,6 @@ public String getName() {
388392
}
389393

390394
enum MyEnum {
391-
INSTANCE;
395+
INSTANCE
392396
}
393397
}

src/main/antora/modules/ROOT/pages/jdbc/query-methods.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,9 @@ Properties that don't have a matching column in the result will not be set.
169169
The query is used for populating the aggregate root, embedded entities and one-to-one relationships including arrays of primitive types which get stored and loaded as SQL-array-types.
170170
Separate queries are generated for maps, lists, sets and arrays of entities.
171171

172+
WARNING: Note that String-based queries do not support pagination nor accept `Sort`, `PageRequest`, and `Limit` as a query parameter as for these queries the query would be required to be rewritten.
173+
If you want to apply limiting, please express this intent using SQL and bind the appropriate parameters to the query yourself.
174+
172175
Queries may contain SpEL expressions where bind variables are allowed.
173176
Such a SpEL expression will get replaced with a bind variable and the variable gets bound to the result of the SpEL expression.
174177

src/main/antora/modules/ROOT/pages/r2dbc/query-methods.adoc

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,27 @@ Therefore also fields with auditing annotations do not get updated if they don't
182182

183183
Alternatively, you can add custom modifying behavior by using the facilities described in xref:repositories/custom-implementations.adoc[Custom Implementations for Spring Data Repositories].
184184

185+
[[r2dbc.query-methods.at-query]]
186+
== Using `@Query`
187+
188+
The following example shows how to use `@Query` to declare a query method:
189+
190+
.Declare a query method by using @Query
191+
[source,java]
192+
----
193+
interface UserRepository extends ReactiveCrudRepository<User, Long> {
194+
195+
@Query("select firstName, lastName from User u where u.emailAddress = :email")
196+
Flux<User> findByEmailAddress(@Param("email") String email);
197+
}
198+
----
199+
200+
WARNING: Note that String-based queries do not support pagination nor accept `Sort`, `PageRequest`, and `Limit` as a query parameter as for these queries the query would be required to be rewritten.
201+
If you want to apply limiting, please express this intent using SQL and bind the appropriate parameters to the query yourself.
202+
203+
NOTE: Spring fully supports Java 8’s parameter name discovery based on the `-parameters` compiler flag.
204+
By using this flag in your build as an alternative to debug information, you can omit the `@Param` annotation for named parameters.
205+
185206
[[r2dbc.repositories.queries.spel]]
186207
=== Queries with SpEL Expressions
187208

0 commit comments

Comments
 (0)