From 9672b30e789796009c8c73b74d85d858c85555e0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 22 Jan 2025 14:15:39 +0100 Subject: [PATCH] =?UTF-8?q?Document=20that=20fluent=20`findBy(=E2=80=A6)`?= =?UTF-8?q?=20queries=20must=20return=20a=20result.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes #3294 --- .../repository/JpaSpecificationExecutor.java | 8 +++++++- .../support/QuerydslJpaPredicateExecutor.java | 11 ++++++++++- .../support/SimpleJpaRepository.java | 11 ++++++++++- .../jpa/repository/UserRepositoryTests.java | 19 ++++++++++++++----- 4 files changed, 41 insertions(+), 8 deletions(-) diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java index 3abd83b2bf..4d18351a99 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java @@ -23,6 +23,7 @@ import java.util.Optional; import java.util.function.Function; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -122,11 +123,16 @@ public interface JpaSpecificationExecutor { /** * Returns entities matching the given {@link Specification} applying the {@code queryFunction} that defines the query * and its result type. + *

+ * The query object used with {@code queryFunction} is only valid inside the {@code findBy(…)} method call. This + * requires the query function to return a query result and not the {@link FluentQuery} object itself to ensure the + * query is executed inside the {@code findBy(…)} method. * * @param spec must not be null. * @param queryFunction the query function defining projection, sorting, and the result type - * @return all entities matching the given Example. + * @return all entities matching the given specification. * @since 3.0 + * @throws InvalidDataAccessApiUsageException if the query function returns the {@link FluentQuery} instance. */ R findBy(Specification spec, Function, R> queryFunction); diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java index ad0762f78d..7b31a6ce5c 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/QuerydslJpaPredicateExecutor.java @@ -24,6 +24,7 @@ import java.util.function.Function; import org.springframework.dao.IncorrectResultSizeDataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.OffsetScrollPosition; import org.springframework.data.domain.Page; @@ -41,6 +42,7 @@ import org.springframework.data.querydsl.EntityPathResolver; import org.springframework.data.querydsl.QSort; import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.lang.Nullable; @@ -245,7 +247,14 @@ public R findBy(Predicate predicate, Function) fluentQuery); + R result = queryFunction.apply((FetchableFluentQuery) fluentQuery); + + if (result instanceof FluentQuery) { + throw new InvalidDataAccessApiUsageException( + "findBy(…) queries must result a query result and not the FluentQuery object to ensure that queries are executed within the scope of the findBy(…) method"); + } + + return result; } @Override diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index c217aa54c8..8c69552c77 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -42,6 +42,7 @@ import java.util.function.BiConsumer; import java.util.function.Function; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Example; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.OffsetScrollPosition; @@ -62,6 +63,7 @@ import org.springframework.data.jpa.support.PageableUtils; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.query.FluentQuery; import org.springframework.data.repository.query.FluentQuery.FetchableFluentQuery; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.data.util.ProxyUtils; @@ -539,7 +541,14 @@ private R doFindBy(Specification spec, Class domainClass, FetchableFluentQueryBySpecification fluentQuery = new FetchableFluentQueryBySpecification<>(spec, domainClass, finder, scrollDelegate, this::count, this::exists, this.entityManager, getProjectionFactory()); - return queryFunction.apply((FetchableFluentQuery) fluentQuery); + R result = queryFunction.apply((FetchableFluentQuery) fluentQuery); + + if (result instanceof FluentQuery) { + throw new InvalidDataAccessApiUsageException( + "findBy(…) queries must result a query result and not the FluentQuery object to ensure that queries are executed within the scope of the findBy(…) method"); + } + + return result; } @Override diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index cc201a01b7..11df50869d 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -41,6 +41,7 @@ import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.function.Function; import java.util.stream.IntStream; import java.util.stream.Stream; @@ -2352,6 +2353,14 @@ void findByFluentExampleWithSorting() { assertThat(users).containsExactly(thirdUser, firstUser, fourthUser); } + @Test // GH-3294 + void findByFluentFailsReturningFluentQuery() { + + User prototype = new User(); + assertThatExceptionOfType(InvalidDataAccessApiUsageException.class) + .isThrownBy(() -> repository.findBy(of(prototype), Function.identity())); + } + @Test // GH-2294 void findByFluentExampleFirstValue() { @@ -2449,13 +2458,13 @@ void findByFluentExampleWithInterfaceBasedProjectionUsingSpEL() { prototype.setFirstname("v"); List users = repository.findBy( - of(prototype, - matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname", - GenericPropertyMatcher::contains)), // - q -> q.as(UserProjectionUsingSpEL.class).all()); + of(prototype, + matching().withIgnorePaths("age", "createdAt", "active").withMatcher("firstname", + GenericPropertyMatcher::contains)), // + q -> q.as(UserProjectionUsingSpEL.class).all()); assertThat(users).extracting(UserProjectionUsingSpEL::hello) - .contains(new GreetingsFrom().groot(firstUser.getFirstname())); + .contains(new GreetingsFrom().groot(firstUser.getFirstname())); } @Test // GH-2294