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 3abd83b2b..4d18351a9 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.List; 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 ad0762f78..7b31a6ce5 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.BiFunction; 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.projection.SpelAwareProxyProjectionFactory; 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 class QuerydslJpaPredicateExecutor implements QuerydslPredicateExecuto 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/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 ba5daeea0..759752f12 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.Optional; 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.repository.support.QueryHints.NoHints; 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; @@ -534,7 +536,14 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation 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 1c365cfb1..b4df229da 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 @@ -42,6 +42,7 @@ import java.util.List; 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; @@ -2355,6 +2356,14 @@ class UserRepositoryTests { 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() { @@ -2452,13 +2461,13 @@ class UserRepositoryTests { 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