Polishing.

Add tests, update documentation, add support for SQL ResultSet mapping.

See #3155
Original pull request: #3353
This commit is contained in:
Mark Paluch
2024-06-21 14:34:47 +02:00
parent 594534150e
commit 3e9d39445d
9 changed files with 150 additions and 65 deletions

View File

@@ -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.
* <p>
* Specifically {@code @NativeQuery} is a <em>composed annotation</em> that
* acts as a shortcut for {@code @Query(nativeQuery = true)}.
* Specifically {@code @NativeQuery} is a <em>composed annotation</em> that acts as a shortcut for
* {@code @Query(nativeQuery = true)} for most attributes.
* <p>
* 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 "";
/**
* Alias for {@link Query#countQuery()}
*/
@AliasFor(annotation = Query.class)
String countQuery() 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#countProjection()}
*/
@AliasFor(annotation = Query.class)
String countProjection() 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#name()}
*/
@AliasFor(annotation = Query.class)
String name() 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#countName()}
*/
@AliasFor(annotation = Query.class)
String countName() 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#queryRewriter()}
*/
@AliasFor(annotation = Query.class)
Class<? extends QueryRewriter> queryRewriter() default QueryRewriter.IdentityQueryRewriter.class;
/**
* 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<? extends QueryRewriter> queryRewriter() default QueryRewriter.IdentityQueryRewriter.class;
/**
* Name of the {@link jakarta.persistence.SqlResultSetMapping @SqlResultSetMapping(name)} to apply for this query.
*/
String sqlResultSetMapping() default "";
}

View File

@@ -24,9 +24,9 @@ import java.lang.annotation.Target;
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.
* <p>
* 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 @@ public @interface Query {
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 @@ public @interface Query {
/**
* 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 @@ public @interface Query {
/**
* 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 "";

View File

@@ -233,6 +233,15 @@ public class JpaQueryMethod extends QueryMethod {
return extractor;
}
/**
* Returns the {@link Method}.
*
* @return
*/
Method getMethod() {
return method;
}
/**
* Returns the actual return type of the method.
*

View File

@@ -19,8 +19,11 @@ import jakarta.persistence.EntityManager;
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.RepositoryQuery;
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 @@ import org.springframework.lang.Nullable;
*/
final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
private final @Nullable String sqlResultSetMapping;
private final boolean queryForEntity;
/**
@@ -59,6 +65,10 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
super(method, em, queryString, countQueryString, rewriter, evaluationContextProvider, parser);
MergedAnnotations annotations = MergedAnnotations.from(method.getMethod());
MergedAnnotation<NativeQuery> 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 @@ final class NativeJpaQuery extends AbstractStringBasedJpaQuery {
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

View File

@@ -83,8 +83,11 @@ import java.util.Set;
@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 @@ import java.util.Set;
@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;

View File

@@ -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 @@ class EclipseLinkNamespaceUserRepositoryTests extends NamespaceUserRepositoryTes
@Disabled
@Override
@Test // DATAJPA-980
void supportsProjectionsWithNativeQueries() {}
void supportsInterfaceProjectionsWithNativeQueries() {}
/**
* Ignored until https://bugs.eclipse.org/bugs/show_bug.cgi?id=525319 is fixed.

View File

@@ -2943,7 +2943,7 @@ class UserRepositoryTests {
}
@Test // DATAJPA-980
void supportsProjectionsWithNativeQueries() {
void supportsInterfaceProjectionsWithNativeQueries() {
flushTestUsers();
@@ -2971,6 +2971,19 @@ class UserRepositoryTests {
.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() {

View File

@@ -41,6 +41,7 @@ import org.springframework.data.jpa.domain.sample.User;
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 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
Slice<User> 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 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
@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 @@ public interface UserRepository extends JpaRepository<User, Integer>, JpaSpecifi
interface IdOnly {
int getId();
}
}

View File

@@ -185,8 +185,7 @@ you can make any alterations at the last moment.
----
public interface MyRepository extends JpaRepository<User, Long> {
@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<User> 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<User, Long> {
@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<User, Long> {
@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<User> findByLastname(String lastname, Pageable pageable);
}
----