Skip to content

Commit

Permalink
Polishing.
Browse files Browse the repository at this point in the history
Add tests, update documentation, add support for SQL ResultSet mapping.

See #3155
Original pull request: #3353
  • Loading branch information
mp911de committed Jun 26, 2024
1 parent 5945341 commit 3e9d394
Show file tree
Hide file tree
Showing 9 changed files with 150 additions and 65 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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)} for most attributes.
* <p>
* Specifically {@code @NativeQuery} is a <em>composed annotation</em> 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<? extends QueryRewriter> queryRewriter() default QueryRewriter.IdentityQueryRewriter.class;

/**
* 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 "";
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <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
Expand All @@ -48,15 +48,15 @@
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.
*/
String countQuery() default "";

/**
* 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
Expand All @@ -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 "";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,19 @@
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;
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}
Expand All @@ -42,6 +46,8 @@
*/
final class NativeJpaQuery extends AbstractStringBasedJpaQuery {

private final @Nullable String sqlResultSetMapping;

private final boolean queryForEntity;

/**
Expand All @@ -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<NativeQuery> annotation = annotations.get(NativeQuery.class);
this.sqlResultSetMapping = annotation.isPresent() ? annotation.getString("sqlResultSetMapping") : null;

this.queryForEntity = getQueryMethod().isQueryForEntity();

Parameters<?, ?> parameters = method.getParameters();
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)"),
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2943,7 +2943,7 @@ void duplicateSpelsWorkAsIntended() {
}

@Test // DATAJPA-980
void supportsProjectionsWithNativeQueries() {
void supportsInterfaceProjectionsWithNativeQueries() {

flushTestUsers();

Expand Down Expand Up @@ -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() {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -405,7 +406,7 @@ Window<User> findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(S
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
Expand Down Expand Up @@ -555,8 +556,12 @@ List<User> 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
Expand Down Expand Up @@ -729,4 +734,5 @@ interface EmailOnly {
interface IdOnly {
int getId();
}

}
Loading

0 comments on commit 3e9d394

Please sign in to comment.