diff --git a/pom.xml b/pom.xml index e05684db9e..9654a0f5ee 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jpa - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAJPA-928-SNAPSHOT Spring Data JPA Spring Data module for JPA repositories. diff --git a/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java index 72be4f8eaa..277954da28 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java @@ -34,6 +34,7 @@ * * @author Thomas Darimont * @author Oliver Gierke + * @author Jens Schauder */ final class NativeJpaQuery extends AbstractStringBasedJpaQuery { @@ -53,13 +54,10 @@ public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryStrin super(method, em, queryString, evaluationContextProvider, parser); Parameters parameters = method.getParameters(); - boolean hasPagingOrSortingParameter = parameters.hasPageableParameter() || parameters.hasSortParameter(); - boolean containsPageableOrSortInQueryExpression = queryString.contains("#pageable") - || queryString.contains("#sort"); - if (hasPagingOrSortingParameter && !containsPageableOrSortInQueryExpression) { + if (parameters.hasSortParameter() && !queryString.contains("#sort")) { throw new InvalidJpaQueryMethodException( - "Cannot use native queries with dynamic sorting and/or pagination in method " + method); + "Cannot use native queries with dynamic sorting in method " + method); } this.resultType = getTypeToQueryFor(); diff --git a/src/test/java/org/springframework/data/jpa/domain/sample/User.java b/src/test/java/org/springframework/data/jpa/domain/sample/User.java index f77e20e6f5..cf19f8126c 100644 --- a/src/test/java/org/springframework/data/jpa/domain/sample/User.java +++ b/src/test/java/org/springframework/data/jpa/domain/sample/User.java @@ -20,29 +20,7 @@ import java.util.HashSet; import java.util.Set; -import javax.persistence.CascadeType; -import javax.persistence.Column; -import javax.persistence.ElementCollection; -import javax.persistence.Embedded; -import javax.persistence.Entity; -import javax.persistence.GeneratedValue; -import javax.persistence.GenerationType; -import javax.persistence.Id; -import javax.persistence.Lob; -import javax.persistence.ManyToMany; -import javax.persistence.ManyToOne; -import javax.persistence.NamedAttributeNode; -import javax.persistence.NamedEntityGraph; -import javax.persistence.NamedEntityGraphs; -import javax.persistence.NamedQuery; -import javax.persistence.NamedStoredProcedureQueries; -import javax.persistence.NamedStoredProcedureQuery; -import javax.persistence.NamedSubgraph; -import javax.persistence.ParameterMode; -import javax.persistence.StoredProcedureParameter; -import javax.persistence.Table; -import javax.persistence.Temporal; -import javax.persistence.TemporalType; +import javax.persistence.*; /** * Domain class representing a person emphasizing the use of {@code AbstractEntity}. No declaration of an id is @@ -51,6 +29,7 @@ * @author Oliver Gierke * @author Thomas Darimont * @author Christoph Strobl + * @author Jens Schauder */ @Entity @NamedEntityGraphs({ @NamedEntityGraph(name = "User.overview", attributeNodes = { @NamedAttributeNode("roles") }), @@ -84,6 +63,23 @@ @NamedStoredProcedureQuery(name = "User.plus1IO", procedureName = "plus1inout", parameters = { @StoredProcedureParameter(mode = ParameterMode.IN, name = "arg", type = Integer.class), @StoredProcedureParameter(mode = ParameterMode.OUT, name = "res", type = Integer.class) }) + +// Annotations for native Query with pageable +@SqlResultSetMappings({ + @SqlResultSetMapping(name = "SqlResultSetMapping.count", columns = @ColumnResult(name = "cnt")) +}) +@NamedNativeQueries({ + @NamedNativeQuery( + name = "User.findByNativeNamedQueryWithPageable", + resultClass = User.class, + query = "SELECT * FROM SD_USER ORDER BY UCASE(firstname)" + ), + @NamedNativeQuery( + name = "User.findByNativeNamedQueryWithPageable.count", + resultSetMapping = "SqlResultSetMapping.count", + query = "SELECT count(*) AS cnt FROM SD_USER" + ) +}) @Table(name = "SD_User") public class User { diff --git a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java index a2543a0236..7bfd7af365 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -40,6 +40,7 @@ import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; +import org.assertj.core.api.SoftAssertions; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; @@ -50,8 +51,6 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher; -import org.springframework.data.domain.ExampleMatcher.GenericPropertyMatcher; -import org.springframework.data.domain.ExampleMatcher.StringMatcher; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.PageRequest; @@ -60,6 +59,7 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; +import org.springframework.data.domain.ExampleMatcher.*; import org.springframework.data.jpa.domain.Specification; import org.springframework.data.jpa.domain.sample.Address; import org.springframework.data.jpa.domain.sample.Role; @@ -2059,8 +2059,8 @@ public void supportsProjectionsWithNativeQueries() { assertThat(result.getLastname()).isEqualTo(user.getLastname()); } - @Test //DATAJPA-1235 - public void handlesColonsFollowedByIntegerInStringLiteral(){ + @Test // DATAJPA-1235 + public void handlesColonsFollowedByIntegerInStringLiteral() { String firstName = "aFirstName"; @@ -2077,6 +2077,56 @@ public void handlesColonsFollowedByIntegerInStringLiteral(){ assertThat(users).extracting(User::getId).containsExactly(expected.getId()); } + // DATAJPA-928 + @Test + public void executeNativeQueryWithPage() { + + flushTestUsers(); + + Page firstPage = repository.findByNativeNamedQueryWithPageable(new PageRequest(0, 3)); + Page secondPage = repository.findByNativeNamedQueryWithPageable(new PageRequest(1, 3)); + + SoftAssertions softly = new SoftAssertions(); + + assertThat(firstPage.getTotalElements()).isEqualTo(4L); + assertThat(firstPage.getNumberOfElements()).isEqualTo(3); + assertThat(firstPage.getContent()) // + .extracting(User::getFirstname) // + .containsExactly("Dave", "Joachim", "kevin"); + + assertThat(secondPage.getTotalElements()).isEqualTo(4L); + assertThat(secondPage.getNumberOfElements()).isEqualTo(1); + assertThat(secondPage.getContent()) // + .extracting(User::getFirstname) // + .containsExactly("Oliver"); + + softly.assertAll(); + } + + // DATAJPA-928 + @Test + public void executeNativeQueryWithPageWorkaround() { + + flushTestUsers(); + + Page firstPage = repository.findByNativeQueryWithPageable(new PageRequest(0, 3)); + Page secondPage = repository.findByNativeQueryWithPageable(new PageRequest(1, 3)); + + SoftAssertions softly = new SoftAssertions(); + + assertThat(firstPage.getTotalElements()).isEqualTo(4L); + assertThat(firstPage.getNumberOfElements()).isEqualTo(3); + assertThat(firstPage.getContent()) // + .containsExactly("Dave", "Joachim", "kevin"); + + assertThat(secondPage.getTotalElements()).isEqualTo(4L); + assertThat(secondPage.getNumberOfElements()).isEqualTo(1); + assertThat(secondPage.getContent()) // + .containsExactly("Oliver"); + + softly.assertAll(); + } + private Page executeSpecWithSort(Sort sort) { flushTestUsers(); diff --git a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java index ef00842040..4e01f01da5 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/JpaQueryLookupStrategyUnitTests.java @@ -15,26 +15,23 @@ */ package org.springframework.data.jpa.repository.query; -import static org.hamcrest.CoreMatchers.*; -import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.*; import java.lang.reflect.Method; +import java.util.List; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.metamodel.Metamodel; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.data.jpa.domain.sample.User; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.Query; @@ -53,6 +50,7 @@ * * @author Oliver Gierke * @author Thomas Darimont + * @author Jens Schauder */ @RunWith(MockitoJUnitRunner.class) public class JpaQueryLookupStrategyUnitTests { @@ -65,8 +63,6 @@ public class JpaQueryLookupStrategyUnitTests { @Mock Metamodel metamodel; @Mock ProjectionFactory projectionFactory; - public @Rule ExpectedException exception = ExpectedException.none(); - @Before public void setUp() { @@ -87,12 +83,9 @@ public void invalidAnnotatedQueryCausesException() throws Exception { Throwable reference = new RuntimeException(); when(em.createQuery(anyString())).thenThrow(reference); - try { - strategy.resolveQuery(method, metadata, projectionFactory, namedQueries); - } catch (Exception e) { - assertThat(e, is(instanceOf(IllegalArgumentException.class))); - assertThat(e.getCause(), is(reference)); - } + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> strategy.resolveQuery(method, metadata, projectionFactory, namedQueries)) + .withCause(reference); } @Test // DATAJPA-554 @@ -100,14 +93,13 @@ public void sholdThrowMorePreciseExceptionIfTryingToUsePaginationInNativeQueries QueryLookupStrategy strategy = JpaQueryLookupStrategy.create(em, Key.CREATE_IF_NOT_FOUND, extractor, EVALUATION_CONTEXT_PROVIDER); - Method method = UserRepository.class.getMethod("findByInvalidNativeQuery", String.class, Pageable.class); + Method method = UserRepository.class.getMethod("findByInvalidNativeQuery", String.class, Sort.class); RepositoryMetadata metadata = new DefaultRepositoryMetadata(UserRepository.class); - exception.expect(InvalidJpaQueryMethodException.class); - exception.expectMessage("Cannot use native queries with dynamic sorting and/or pagination in method"); - exception.expectMessage(method.toString()); - - strategy.resolveQuery(method, metadata, projectionFactory, namedQueries); + assertThatExceptionOfType(InvalidJpaQueryMethodException.class) + .isThrownBy(() -> strategy.resolveQuery(method, metadata, projectionFactory, namedQueries)) + .withMessageContaining("Cannot use native queries with dynamic sorting in method") + .withMessageContaining(method.toString()); } interface UserRepository extends Repository { @@ -116,6 +108,6 @@ interface UserRepository extends Repository { User findByFoo(String foo); @Query(value = "select u.* from User u", nativeQuery = true) - Page findByInvalidNativeQuery(String param, Pageable page); + List findByInvalidNativeQuery(String param, Sort sort); } } diff --git a/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java b/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java index a8cad5954d..3ad4fc260b 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/query/SimpleJpaQueryUnitTests.java @@ -17,7 +17,9 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.*; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.*; import java.lang.reflect.Method; @@ -59,6 +61,7 @@ * * @author Oliver Gierke * @author Thomas Darimont + * @author Jens Schauder */ @RunWith(MockitoJUnitRunner.Silent.class) public class SimpleJpaQueryUnitTests { @@ -153,13 +156,6 @@ public void rejectsNativeQueryWithDynamicSort() throws Exception { createJpaQuery(method); } - @Test(expected = InvalidJpaQueryMethodException.class) // DATAJPA-554 - public void rejectsNativeQueryWithPageable() throws Exception { - - Method method = SampleRepository.class.getMethod("findNativeByLastname", String.class, Pageable.class); - createJpaQuery(method); - } - @Test // DATAJPA-352 @SuppressWarnings("unchecked") public void doesNotValidateCountQueryIfNotPagingMethod() throws Exception { diff --git a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index 66944af650..12f29fdf5a 100644 --- a/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -513,6 +513,15 @@ List findUsersByFirstnameForSpELExpressionWithParameterIndexOnlyWithEntity @Query("SELECT u FROM User u where u.firstname >= ?1 and u.lastname = '000:1'") List queryWithIndexedParameterAndColonFollowedByIntegerInString(String firstname); + + // DATAJPA-928 + Page findByNativeNamedQueryWithPageable(Pageable pageable); + + + // DATAJPA-928 + @Query(value = "SELECT firstname FROM SD_User ORDER BY UCASE(firstname)", countQuery = "SELECT count(*) FROM SD_User", nativeQuery = true) + Page findByNativeQueryWithPageable(@Param("pageable") Pageable pageable); + static interface RolesAndFirstname { String getFirstname();