Skip to content

Commit 3e9d394

Browse files
committed
Polishing.
Add tests, update documentation, add support for SQL ResultSet mapping. See #3155 Original pull request: #3353
1 parent 5945341 commit 3e9d394

File tree

9 files changed

+150
-65
lines changed

9 files changed

+150
-65
lines changed

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/NativeQuery.java

+60-39
Original file line numberDiff line numberDiff line change
@@ -15,62 +15,83 @@
1515
*/
1616
package org.springframework.data.jpa.repository;
1717

18-
import org.springframework.core.annotation.AliasFor;
19-
import org.springframework.data.annotation.QueryAnnotation;
18+
import java.lang.annotation.Documented;
19+
import java.lang.annotation.ElementType;
20+
import java.lang.annotation.Retention;
21+
import java.lang.annotation.RetentionPolicy;
22+
import java.lang.annotation.Target;
2023

21-
import java.lang.annotation.*;
24+
import org.springframework.core.annotation.AliasFor;
2225

2326
/**
24-
* Annotation to declare native queries directly on repository methods.
27+
* Annotation to declare native queries directly on repository query methods.
28+
* <p>
29+
* Specifically {@code @NativeQuery} is a <em>composed annotation</em> that acts as a shortcut for
30+
* {@code @Query(nativeQuery = true)} for most attributes.
2531
* <p>
26-
* Specifically {@code @NativeQuery} is a <em>composed annotation</em> that
27-
* acts as a shortcut for {@code @Query(nativeQuery = true)}.
32+
* This annotation defines {@code sqlResultSetMapping} to apply JPA SQL ResultSet mapping for native queries. Make sure
33+
* to use the corresponding return type as defined in {@code @SqlResultSetMapping}. When using named native queries,
34+
* define SQL result set mapping through {@code @NamedNativeQuery(resultSetMapping=…)} as named queries do not accept
35+
* {@code sqlResultSetMapping}.
2836
*
2937
* @author Danny van den Elshout
30-
* @since 3.3
38+
* @author Mark Paluch
39+
* @since 3.4
3140
* @see Query
41+
* @see Modifying
3242
*/
33-
3443
@Retention(RetentionPolicy.RUNTIME)
3544
@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE })
36-
@QueryAnnotation
3745
@Documented
3846
@Query(nativeQuery = true)
3947
public @interface NativeQuery {
4048

41-
/**
42-
* Alias for {@link Query#value()}
43-
*/
44-
@AliasFor(annotation = Query.class)
45-
String value() default "";
49+
/**
50+
* Defines the native query to be executed when the annotated method is called. Alias for {@link Query#value()}.
51+
*/
52+
@AliasFor(annotation = Query.class)
53+
String value() default "";
54+
55+
/**
56+
* Defines a special count query that shall be used for pagination queries to look up the total number of elements for
57+
* a page. If none is configured we will derive the count query from the original query or {@link #countProjection()}
58+
* query if any. Alias for {@link Query#countQuery()}.
59+
*/
60+
@AliasFor(annotation = Query.class)
61+
String countQuery() default "";
4662

47-
/**
48-
* Alias for {@link Query#countQuery()}
49-
*/
50-
@AliasFor(annotation = Query.class)
51-
String countQuery() default "";
63+
/**
64+
* Defines the projection part of the count query that is generated for pagination. If neither {@link #countQuery()}
65+
* nor {@code countProjection()} is configured we will derive the count query from the original query. Alias for
66+
* {@link Query#countProjection()}.
67+
*/
68+
@AliasFor(annotation = Query.class)
69+
String countProjection() default "";
5270

53-
/**
54-
* Alias for {@link Query#countProjection()}
55-
*/
56-
@AliasFor(annotation = Query.class)
57-
String countProjection() default "";
71+
/**
72+
* The named query to be used. If not defined, a {@link jakarta.persistence.NamedQuery} with name of
73+
* {@code ${domainClass}.${queryMethodName}} will be used. Alias for {@link Query#name()}.
74+
*/
75+
@AliasFor(annotation = Query.class)
76+
String name() default "";
5877

59-
/**
60-
* Alias for {@link Query#name()}
61-
*/
62-
@AliasFor(annotation = Query.class)
63-
String name() default "";
78+
/**
79+
* Returns the name of the {@link jakarta.persistence.NamedQuery} to be used to execute count queries when pagination
80+
* is used. Will default to the named query name configured suffixed by {@code .count}. Alias for
81+
* {@link Query#countName()}.
82+
*/
83+
@AliasFor(annotation = Query.class)
84+
String countName() default "";
6485

65-
/**
66-
* Alias for {@link Query#countName()}
67-
*/
68-
@AliasFor(annotation = Query.class)
69-
String countName() default "";
86+
/**
87+
* Define a {@link QueryRewriter} that should be applied to the query string after the query is fully assembled. Alias
88+
* for {@link Query#queryRewriter()}.
89+
*/
90+
@AliasFor(annotation = Query.class)
91+
Class<? extends QueryRewriter> queryRewriter() default QueryRewriter.IdentityQueryRewriter.class;
7092

71-
/**
72-
* Alias for {@link Query#queryRewriter()}
73-
*/
74-
@AliasFor(annotation = Query.class)
75-
Class<? extends QueryRewriter> queryRewriter() default QueryRewriter.IdentityQueryRewriter.class;
93+
/**
94+
* Name of the {@link jakarta.persistence.SqlResultSetMapping @SqlResultSetMapping(name)} to apply for this query.
95+
*/
96+
String sqlResultSetMapping() default "";
7697
}

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/Query.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@
2424
import org.springframework.data.annotation.QueryAnnotation;
2525

2626
/**
27-
* Annotation to declare finder queries directly on repository methods.
27+
* Annotation to declare finder queries directly on repository query methods.
2828
* <p>
29-
* When using a native query {@link NativeQuery @NativeQuery} variant is available.
29+
* When using a native query, a {@link NativeQuery @NativeQuery} variant is available.
3030
*
3131
* @author Oliver Gierke
3232
* @author Thomas Darimont
@@ -48,15 +48,15 @@
4848
String value() default "";
4949

5050
/**
51-
* Defines a special count query that shall be used for pagination queries to lookup the total number of elements for
51+
* Defines a special count query that shall be used for pagination queries to look up the total number of elements for
5252
* a page. If none is configured we will derive the count query from the original query or {@link #countProjection()}
5353
* query if any.
5454
*/
5555
String countQuery() default "";
5656

5757
/**
5858
* Defines the projection part of the count query that is generated for pagination. If neither {@link #countQuery()}
59-
* nor {@link #countProjection()} is configured we will derive the count query from the original query.
59+
* nor {@code countProjection()} is configured we will derive the count query from the original query.
6060
*
6161
* @return
6262
* @since 1.6
@@ -70,7 +70,7 @@
7070

7171
/**
7272
* The named query to be used. If not defined, a {@link jakarta.persistence.NamedQuery} with name of
73-
* {@code $ domainClass}.${queryMethodName}} will be used.
73+
* {@code ${domainClass}.${queryMethodName}} will be used.
7474
*/
7575
String name() default "";
7676

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/JpaQueryMethod.java

+9
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,15 @@ QueryExtractor getQueryExtractor() {
233233
return extractor;
234234
}
235235

236+
/**
237+
* Returns the {@link Method}.
238+
*
239+
* @return
240+
*/
241+
Method getMethod() {
242+
return method;
243+
}
244+
236245
/**
237246
* Returns the actual return type of the method.
238247
*

spring-data-jpa/src/main/java/org/springframework/data/jpa/repository/query/NativeJpaQuery.java

+17-3
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,19 @@
1919
import jakarta.persistence.Query;
2020
import jakarta.persistence.Tuple;
2121

22+
import org.springframework.core.annotation.MergedAnnotation;
23+
import org.springframework.core.annotation.MergedAnnotations;
2224
import org.springframework.data.domain.Pageable;
2325
import org.springframework.data.domain.Sort;
26+
import org.springframework.data.jpa.repository.NativeQuery;
2427
import org.springframework.data.jpa.repository.QueryRewriter;
2528
import org.springframework.data.repository.query.Parameters;
2629
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
2730
import org.springframework.data.repository.query.RepositoryQuery;
2831
import org.springframework.data.repository.query.ReturnedType;
2932
import org.springframework.expression.spel.standard.SpelExpressionParser;
3033
import org.springframework.lang.Nullable;
34+
import org.springframework.util.ObjectUtils;
3135

3236
/**
3337
* {@link RepositoryQuery} implementation that inspects a {@link org.springframework.data.repository.query.QueryMethod}
@@ -42,6 +46,8 @@
4246
*/
4347
final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
4448

49+
private final @Nullable String sqlResultSetMapping;
50+
4551
private final boolean queryForEntity;
4652

4753
/**
@@ -59,6 +65,10 @@ public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryStrin
5965

6066
super(method, em, queryString, countQueryString, rewriter, evaluationContextProvider, parser);
6167

68+
MergedAnnotations annotations = MergedAnnotations.from(method.getMethod());
69+
MergedAnnotation<NativeQuery> annotation = annotations.get(NativeQuery.class);
70+
this.sqlResultSetMapping = annotation.isPresent() ? annotation.getString("sqlResultSetMapping") : null;
71+
6272
this.queryForEntity = getQueryMethod().isQueryForEntity();
6373

6474
Parameters<?, ?> parameters = method.getParameters();
@@ -72,10 +82,14 @@ public NativeJpaQuery(JpaQueryMethod method, EntityManager em, String queryStrin
7282
protected Query createJpaQuery(String queryString, Sort sort, Pageable pageable, ReturnedType returnedType) {
7383

7484
EntityManager em = getEntityManager();
75-
Class<?> type = getTypeToQueryFor(returnedType);
85+
String query = potentiallyRewriteQuery(queryString, sort, pageable);
7686

77-
return type == null ? em.createNativeQuery(potentiallyRewriteQuery(queryString, sort, pageable))
78-
: em.createNativeQuery(potentiallyRewriteQuery(queryString, sort, pageable), type);
87+
if (!ObjectUtils.isEmpty(sqlResultSetMapping)) {
88+
return em.createNativeQuery(query, sqlResultSetMapping);
89+
}
90+
91+
Class<?> type = getTypeToQueryFor(returnedType);
92+
return type == null ? em.createNativeQuery(query) : em.createNativeQuery(query, type);
7993
}
8094

8195
@Nullable

spring-data-jpa/src/test/java/org/springframework/data/jpa/domain/sample/User.java

+25-3
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,11 @@
8383
@StoredProcedureParameter(mode = ParameterMode.OUT, name = "res", type = Integer.class) })
8484

8585
// Annotations for native Query with pageable
86-
@SqlResultSetMappings({
87-
@SqlResultSetMapping(name = "SqlResultSetMapping.count", columns = @ColumnResult(name = "cnt")) })
86+
@SqlResultSetMappings({ @SqlResultSetMapping(name = "SqlResultSetMapping.count", columns = @ColumnResult(name = "cnt")),
87+
@SqlResultSetMapping(name = "emailDto",
88+
classes = { @ConstructorResult(targetClass = User.EmailDto.class,
89+
columns = { @ColumnResult(name = "emailaddress", type = String.class),
90+
@ColumnResult(name = "secondary_email_address", type = String.class) }) }) })
8891
@NamedNativeQueries({
8992
@NamedNativeQuery(name = "User.findByNativeNamedQueryWithPageable", resultClass = User.class,
9093
query = "SELECT * FROM SD_USER ORDER BY UCASE(firstname)"),
@@ -93,7 +96,26 @@
9396
@Table(name = "SD_User")
9497
public class User {
9598

96-
@Id @GeneratedValue(strategy = GenerationType.AUTO) private Integer id;
99+
public static class EmailDto {
100+
private final String one;
101+
private final String two;
102+
103+
public EmailDto(String one, String two) {
104+
this.one = one;
105+
this.two = two;
106+
}
107+
108+
public String getOne() {
109+
return one;
110+
}
111+
112+
public String getTwo() {
113+
return two;
114+
}
115+
}
116+
117+
@Id
118+
@GeneratedValue(strategy = GenerationType.AUTO) private Integer id;
97119
private String firstname;
98120
private String lastname;
99121
private int age;

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/EclipseLinkNamespaceUserRepositoryTests.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515
*/
1616
package org.springframework.data.jpa.repository;
1717

18-
import static org.assertj.core.api.Assertions.assertThat;
18+
import static org.assertj.core.api.Assertions.*;
1919

2020
import jakarta.persistence.Query;
2121

2222
import org.junit.jupiter.api.Disabled;
2323
import org.junit.jupiter.api.Test;
24+
2425
import org.springframework.data.jpa.repository.sample.UserRepository;
2526
import org.springframework.test.context.ContextConfiguration;
2627

@@ -75,7 +76,7 @@ void queryProvidesCorrectNumberOfParametersForNativeQuery() {
7576
@Disabled
7677
@Override
7778
@Test // DATAJPA-980
78-
void supportsProjectionsWithNativeQueries() {}
79+
void supportsInterfaceProjectionsWithNativeQueries() {}
7980

8081
/**
8182
* Ignored until https://bugs.eclipse.org/bugs/show_bug.cgi?id=525319 is fixed.

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java

+14-1
Original file line numberDiff line numberDiff line change
@@ -2943,7 +2943,7 @@ void duplicateSpelsWorkAsIntended() {
29432943
}
29442944

29452945
@Test // DATAJPA-980
2946-
void supportsProjectionsWithNativeQueries() {
2946+
void supportsInterfaceProjectionsWithNativeQueries() {
29472947

29482948
flushTestUsers();
29492949

@@ -2971,6 +2971,19 @@ void supportsProjectionsWithNativeQueriesAndCamelCaseProperty() {
29712971
.isNotNull();
29722972
}
29732973

2974+
@Test // GH-3155
2975+
void supportsResultSetMappingWithNativeQueries() {
2976+
2977+
flushTestUsers();
2978+
2979+
User user = repository.findAll().get(0);
2980+
2981+
User.EmailDto result = repository.findEmailDtoByNativeQuery(user.getId());
2982+
2983+
assertThat(result.getOne()).isEqualTo(user.getEmailAddress());
2984+
assertThat(result.getTwo()).isEqualTo(user.getSecondaryEmailAddress());
2985+
}
2986+
29742987
@Test // GH-3462
29752988
void supportsProjectionsWithNativeQueriesAndUnderscoresColumnNameToCamelCaseProperty() {
29762989

spring-data-jpa/src/test/java/org/springframework/data/jpa/repository/sample/UserRepository.java

+9-3
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import org.springframework.data.jpa.repository.JpaRepository;
4242
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
4343
import org.springframework.data.jpa.repository.Modifying;
44+
import org.springframework.data.jpa.repository.NativeQuery;
4445
import org.springframework.data.jpa.repository.Query;
4546
import org.springframework.data.jpa.repository.QueryHints;
4647
import org.springframework.data.jpa.repository.query.Procedure;
@@ -405,7 +406,7 @@ Window<User> findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S
405406
Slice<User> findTop2UsersBy(Pageable page);
406407

407408
// DATAJPA-506
408-
@Query(value = "select u.binaryData from SD_User u where u.id = ?1", nativeQuery = true)
409+
@NativeQuery("select u.binaryData from SD_User u where u.id = ?1")
409410
byte[] findBinaryDataByIdNative(Integer id);
410411

411412
// DATAJPA-506
@@ -555,8 +556,12 @@ List<User> findUsersByFirstnameForSpELExpressionWithParameterIndexOnlyWithEntity
555556
@Query(value = "SELECT firstname, lastname FROM SD_User WHERE id = ?1", nativeQuery = true)
556557
NameOnly findByNativeQuery(Integer id);
557558

558-
// DATAJPA-1248
559-
@Query(value = "SELECT emailaddress, secondary_email_address FROM SD_User WHERE id = ?1", nativeQuery = true)
559+
// GH-3155
560+
@NativeQuery(value = "SELECT emailaddress, secondary_email_address FROM SD_User WHERE id = ?1",
561+
sqlResultSetMapping = "emailDto")
562+
User.EmailDto findEmailDtoByNativeQuery(Integer id);
563+
564+
@NativeQuery(value = "SELECT emailaddress, secondary_email_address FROM SD_User WHERE id = ?1")
560565
EmailOnly findEmailOnlyByNativeQuery(Integer id);
561566

562567
// DATAJPA-1235
@@ -729,4 +734,5 @@ interface EmailOnly {
729734
interface IdOnly {
730735
int getId();
731736
}
737+
732738
}

0 commit comments

Comments
 (0)