diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java index abbf986f25..47ed58c61a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java @@ -15,62 +15,83 @@ */ package org.springframework.data.jpa.repository; -import org.springframework.core.annotation.AliasFor; -import org.springframework.data.annotation.QueryAnnotation; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; -import java.lang.annotation.*; +import org.springframework.core.annotation.AliasFor; /** - * Annotation to declare native queries directly on repository methods. + * Annotation to declare native queries directly on repository query methods. + *

+ * Specifically {@code @NativeQuery} is a composed annotation that acts as a shortcut for + * {@code @Query(nativeQuery = true)} for most attributes. *

- * Specifically {@code @NativeQuery} is a composed annotation that - * acts as a shortcut for {@code @Query(nativeQuery = true)}. + * This annotation defines {@code sqlResultSetMapping} to apply JPA SQL ResultSet mapping for native queries. Make sure + * to use the corresponding return type as defined in {@code @SqlResultSetMapping}. When using named native queries, + * define SQL result set mapping through {@code @NamedNativeQuery(resultSetMapping=…)} as named queries do not accept + * {@code sqlResultSetMapping}. * * @author Danny van den Elshout - * @since 3.3 + * @author Mark Paluch + * @since 3.4 * @see Query + * @see Modifying */ - @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) -@QueryAnnotation @Documented @Query(nativeQuery = true) public @interface NativeQuery { - /** - * Alias for {@link Query#value()} - */ - @AliasFor(annotation = Query.class) - String value() default ""; + /** + * Defines the native query to be executed when the annotated method is called. Alias for {@link Query#value()}. + */ + @AliasFor(annotation = Query.class) + String value() default ""; + + /** + * Defines a special count query that shall be used for pagination queries to look up the total number of elements for + * a page. If none is configured we will derive the count query from the original query or {@link #countProjection()} + * query if any. Alias for {@link Query#countQuery()}. + */ + @AliasFor(annotation = Query.class) + String countQuery() default ""; - /** - * Alias for {@link Query#countQuery()} - */ - @AliasFor(annotation = Query.class) - String countQuery() default ""; + /** + * Defines the projection part of the count query that is generated for pagination. If neither {@link #countQuery()} + * nor {@code countProjection()} is configured we will derive the count query from the original query. Alias for + * {@link Query#countProjection()}. + */ + @AliasFor(annotation = Query.class) + String countProjection() default ""; - /** - * Alias for {@link Query#countProjection()} - */ - @AliasFor(annotation = Query.class) - String countProjection() default ""; + /** + * The named query to be used. If not defined, a {@link jakarta.persistence.NamedQuery} with name of + * {@code ${domainClass}.${queryMethodName}} will be used. Alias for {@link Query#name()}. + */ + @AliasFor(annotation = Query.class) + String name() default ""; - /** - * Alias for {@link Query#name()} - */ - @AliasFor(annotation = Query.class) - String name() default ""; + /** + * Returns the name of the {@link jakarta.persistence.NamedQuery} to be used to execute count queries when pagination + * is used. Will default to the named query name configured suffixed by {@code .count}. Alias for + * {@link Query#countName()}. + */ + @AliasFor(annotation = Query.class) + String countName() default ""; - /** - * Alias for {@link Query#countName()} - */ - @AliasFor(annotation = Query.class) - String countName() default ""; + /** + * Define a {@link QueryRewriter} that should be applied to the query string after the query is fully assembled. Alias + * for {@link Query#queryRewriter()}. + */ + @AliasFor(annotation = Query.class) + Class queryRewriter() default QueryRewriter.IdentityQueryRewriter.class; - /** - * Alias for {@link Query#queryRewriter()} - */ - @AliasFor(annotation = Query.class) - Class queryRewriter() default QueryRewriter.IdentityQueryRewriter.class; + /** + * Name of the {@link jakarta.persistence.SqlResultSetMapping @SqlResultSetMapping(name)} to apply for this query. + */ + String sqlResultSetMapping() default ""; } diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java index e9010594aa..e43157b40e 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java @@ -24,9 +24,9 @@ import org.springframework.data.annotation.QueryAnnotation; /** - * Annotation to declare finder queries directly on repository methods. + * Annotation to declare finder queries directly on repository query methods. *

- * When using a native query {@link NativeQuery @NativeQuery} variant is available. + * When using a native query, a {@link NativeQuery @NativeQuery} variant is available. * * @author Oliver Gierke * @author Thomas Darimont @@ -48,7 +48,7 @@ String value() default ""; /** - * Defines a special count query that shall be used for pagination queries to lookup the total number of elements for + * Defines a special count query that shall be used for pagination queries to look up the total number of elements for * a page. If none is configured we will derive the count query from the original query or {@link #countProjection()} * query if any. */ @@ -56,7 +56,7 @@ /** * Defines the projection part of the count query that is generated for pagination. If neither {@link #countQuery()} - * nor {@link #countProjection()} is configured we will derive the count query from the original query. + * nor {@code countProjection()} is configured we will derive the count query from the original query. * * @return * @since 1.6 @@ -70,7 +70,7 @@ /** * The named query to be used. If not defined, a {@link jakarta.persistence.NamedQuery} with name of - * {@code $ domainClass}.${queryMethodName}} will be used. + * {@code ${domainClass}.${queryMethodName}} will be used. */ String name() default ""; diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java index b299d87c41..fde3b2cb9a 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java @@ -233,6 +233,15 @@ QueryExtractor getQueryExtractor() { return extractor; } + /** + * Returns the {@link Method}. + * + * @return + */ + Method getMethod() { + return method; + } + /** * Returns the actual return type of the method. * diff --git a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java index a2bb681e13..c0ad69de72 100644 --- a/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java +++ b/spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java @@ -19,8 +19,11 @@ import jakarta.persistence.Query; import jakarta.persistence.Tuple; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.QueryRewriter; import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; @@ -28,6 +31,7 @@ import org.springframework.data.repository.query.ReturnedType; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; /** * {@link RepositoryQuery} implementation that inspects a {@link org.springframework.data.repository.query.QueryMethod} @@ -42,6 +46,8 @@ */ final class NativeJpaQuery extends AbstractStringBasedJpaQuery { + private final @Nullable String sqlResultSetMapping; + private final boolean queryForEntity; /** @@ -59,6 +65,10 @@ public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryStrin super(method, em, queryString, countQueryString, rewriter, evaluationContextProvider, parser); + MergedAnnotations annotations = MergedAnnotations.from(method.getMethod()); + MergedAnnotation annotation = annotations.get(NativeQuery.class); + this.sqlResultSetMapping = annotation.isPresent() ? annotation.getString("sqlResultSetMapping") : null; + this.queryForEntity = getQueryMethod().isQueryForEntity(); Parameters parameters = method.getParameters(); @@ -72,10 +82,14 @@ public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryStrin protected Query createJpaQuery(String queryString, Sort sort, Pageable pageable, ReturnedType returnedType) { EntityManager em = getEntityManager(); - Class type = getTypeToQueryFor(returnedType); + String query = potentiallyRewriteQuery(queryString, sort, pageable); - return type == null ? em.createNativeQuery(potentiallyRewriteQuery(queryString, sort, pageable)) - : em.createNativeQuery(potentiallyRewriteQuery(queryString, sort, pageable), type); + if (!ObjectUtils.isEmpty(sqlResultSetMapping)) { + return em.createNativeQuery(query, sqlResultSetMapping); + } + + Class type = getTypeToQueryFor(returnedType); + return type == null ? em.createNativeQuery(query) : em.createNativeQuery(query, type); } @Nullable diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/User.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/User.java index d98ed10d6a..fcad1f8e89 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/User.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/User.java @@ -83,8 +83,11 @@ @StoredProcedureParameter(mode = ParameterMode.OUT, name = "res", type = Integer.class) }) // Annotations for native Query with pageable -@SqlResultSetMappings({ - @SqlResultSetMapping(name = "SqlResultSetMapping.count", columns = @ColumnResult(name = "cnt")) }) +@SqlResultSetMappings({ @SqlResultSetMapping(name = "SqlResultSetMapping.count", columns = @ColumnResult(name = "cnt")), + @SqlResultSetMapping(name = "emailDto", + classes = { @ConstructorResult(targetClass = User.EmailDto.class, + columns = { @ColumnResult(name = "emailaddress", type = String.class), + @ColumnResult(name = "secondary_email_address", type = String.class) }) }) }) @NamedNativeQueries({ @NamedNativeQuery(name = "User.findByNativeNamedQueryWithPageable", resultClass = User.class, query = "SELECT * FROM SD_USER ORDER BY UCASE(firstname)"), @@ -93,7 +96,26 @@ @Table(name = "SD_User") public class User { - @Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; + public static class EmailDto { + private final String one; + private final String two; + + public EmailDto(String one, String two) { + this.one = one; + this.two = two; + } + + public String getOne() { + return one; + } + + public String getTwo() { + return two; + } + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) private Integer id; private String firstname; private String lastname; private int age; diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkNamespaceUserRepositoryTests.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkNamespaceUserRepositoryTests.java index 6eb1d40dda..a93fb5336b 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkNamespaceUserRepositoryTests.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkNamespaceUserRepositoryTests.java @@ -15,12 +15,13 @@ */ package org.springframework.data.jpa.repository; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; import jakarta.persistence.Query; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; + import org.springframework.data.jpa.repository.sample.UserRepository; import org.springframework.test.context.ContextConfiguration; @@ -75,7 +76,7 @@ void queryProvidesCorrectNumberOfParametersForNativeQuery() { @Disabled @Override @Test // DATAJPA-980 - void supportsProjectionsWithNativeQueries() {} + void supportsInterfaceProjectionsWithNativeQueries() {} /** * Ignored until https://bugs.eclipse.org/bugs/show_bug.cgi?id=525319 is fixed. 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 d549077eec..ed3b763650 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 @@ -2943,7 +2943,7 @@ void duplicateSpelsWorkAsIntended() { } @Test // DATAJPA-980 - void supportsProjectionsWithNativeQueries() { + void supportsInterfaceProjectionsWithNativeQueries() { flushTestUsers(); @@ -2971,6 +2971,19 @@ void supportsProjectionsWithNativeQueriesAndCamelCaseProperty() { .isNotNull(); } + @Test // GH-3155 + void supportsResultSetMappingWithNativeQueries() { + + flushTestUsers(); + + User user = repository.findAll().get(0); + + User.EmailDto result = repository.findEmailDtoByNativeQuery(user.getId()); + + assertThat(result.getOne()).isEqualTo(user.getEmailAddress()); + assertThat(result.getTwo()).isEqualTo(user.getSecondaryEmailAddress()); + } + @Test // GH-3462 void supportsProjectionsWithNativeQueriesAndUnderscoresColumnNameToCamelCaseProperty() { diff --git a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java index 684b49bced..b2251291af 100644 --- a/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java +++ b/spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java @@ -41,6 +41,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.NativeQuery; import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.QueryHints; import org.springframework.data.jpa.repository.query.Procedure; @@ -405,7 +406,7 @@ Window findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S Slice findTop2UsersBy(Pageable page); // DATAJPA-506 - @Query(value = "select u.binaryData from SD_User u where u.id = ?1", nativeQuery = true) + @NativeQuery("select u.binaryData from SD_User u where u.id = ?1") byte[] findBinaryDataByIdNative(Integer id); // DATAJPA-506 @@ -555,8 +556,12 @@ List findUsersByFirstnameForSpELExpressionWithParameterIndexOnlyWithEntity @Query(value = "SELECT firstname, lastname FROM SD_User WHERE id = ?1", nativeQuery = true) NameOnly findByNativeQuery(Integer id); - // DATAJPA-1248 - @Query(value = "SELECT emailaddress, secondary_email_address FROM SD_User WHERE id = ?1", nativeQuery = true) + // GH-3155 + @NativeQuery(value = "SELECT emailaddress, secondary_email_address FROM SD_User WHERE id = ?1", + sqlResultSetMapping = "emailDto") + User.EmailDto findEmailDtoByNativeQuery(Integer id); + + @NativeQuery(value = "SELECT emailaddress, secondary_email_address FROM SD_User WHERE id = ?1") EmailOnly findEmailOnlyByNativeQuery(Integer id); // DATAJPA-1235 @@ -729,4 +734,5 @@ interface EmailOnly { interface IdOnly { int getId(); } + } diff --git a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc index d6fb186acc..475dc05517 100644 --- a/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc +++ b/src/main/antora/modules/ROOT/pages/jpa/query-methods.adoc @@ -185,8 +185,7 @@ you can make any alterations at the last moment. ---- public interface MyRepository extends JpaRepository { - @Query(value = "select original_user_alias.* from SD_USER original_user_alias", - nativeQuery = true, + @NativeQuery(value = "select original_user_alias.* from SD_USER original_user_alias", queryRewriter = MyQueryRewriter.class) List findByNativeQuery(String param); @@ -273,7 +272,7 @@ In the preceding example, the `LIKE` delimiter character (`%`) is recognized, an [[jpa.query-methods.at-query.native]] === Native Queries -The `@Query` annotation allows for running native queries by setting the `nativeQuery` flag to true, as shown in the following example: +Using the `@NativeQuery` annotation allows running native queries, as shown in the following example: .Declare a native query at the query method using @Query ==== @@ -281,24 +280,24 @@ The `@Query` annotation allows for running native queries by setting the `native ---- public interface UserRepository extends JpaRepository { - @Query(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1", nativeQuery = true) + @NativeQuery(value = "SELECT * FROM USERS WHERE EMAIL_ADDRESS = ?1") User findByEmailAddress(String emailAddress); } ---- - ==== +NOTE: The `@NativeQuery` annotation is mostly a composed annotation for `@Query(nativeQuery=true)` but it also provides additional attributes such as `sqlResultSetMapping` to leverage JPA's `@SqlResultSetMapping(…)`. + NOTE: Spring Data JPA does not currently support dynamic sorting for native queries, because it would have to manipulate the actual query declared, which it cannot do reliably for native SQL. You can, however, use native queries for pagination by specifying the count query yourself, as shown in the following example: -.Declare native count queries for pagination at the query method by using `@Query` +.Declare native count queries for pagination at the query method by using `@NativeQuery` ==== [source, java] ---- public interface UserRepository extends JpaRepository { - @Query(value = "SELECT * FROM USERS WHERE LASTNAME = ?1", - countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1", - nativeQuery = true) + @NativeQuery(value = "SELECT * FROM USERS WHERE LASTNAME = ?1", + countQuery = "SELECT count(*) FROM USERS WHERE LASTNAME = ?1") Page findByLastname(String lastname, Pageable pageable); } ----