From 9cc5d981ca07d394a7de3e2e6070ef96788138a3 Mon Sep 17 00:00:00 2001 From: Oliver Gierke Date: Mon, 26 Feb 2018 15:46:16 +0100 Subject: [PATCH] DATAJPA-1273 - Improve projected named query execution to use Tuples. NamedQuery now inspects the ReturnedType of the query method to potentially create a JPA Query instance typed to Tuple for projections onto non JPA managed types. This will cause Tuple instances being returned that allow binding to projection accessors by the aliases defined in the query instead of a plain Object array that will rely on the declaration order of the columns to be returned being in sync with the declaration order of the accessors in the projection interface. Disabled integration tests for EclipseLink as it apparently doesn't support Tuple as result type properly. Added a standalone test case to showcase the issue. --- .../repository/query/AbstractJpaQuery.java | 17 +++++++++++++ .../query/AbstractStringBasedJpaQuery.java | 12 +++------ .../data/jpa/repository/query/NamedQuery.java | 7 +++++- .../data/jpa/domain/sample/User.java | 7 +++++- .../EclipseLinkMetamodelIntegrationTests.java | 8 ++++++ .../MetamodelIntegrationTests.java | 25 ++++++++++++++++++- ...lipseLinkNamespaceUserRepositoryTests.java | 12 ++++++--- .../jpa/repository/UserRepositoryTests.java | 13 ++++++++++ .../jpa/repository/sample/UserRepository.java | 3 +++ 9 files changed, 89 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java index faf1a070c..1045375a0 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/AbstractJpaQuery.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -234,6 +235,22 @@ public abstract class AbstractJpaQuery implements RepositoryQuery { return method.applyHintsToCountQuery() ? applyHints(countQuery, method) : countQuery; } + /** + * Returns the type to be used when creating the JPA query. + * + * @return + * @since 2.0.5 + */ + protected Optional> getTypeToRead() { + + ResultProcessor resultFactory = getQueryMethod().getResultProcessor(); + ReturnedType returnedType = resultFactory.getReturnedType(); + + return returnedType.isProjecting() && !getMetamodel().isJpaManaged(returnedType.getReturnedType()) + ? Optional.of(Tuple.class) + : Optional.empty(); + } + /** * Creates a {@link Query} instance for the given values. * diff --git a/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java index a8e09349a..d986f9237 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/AbstractStringBasedJpaQuery.java @@ -19,13 +19,10 @@ import static org.springframework.data.jpa.repository.query.QueryParameterSetter import javax.persistence.EntityManager; import javax.persistence.Query; -import javax.persistence.Tuple; import org.springframework.data.repository.query.EvaluationContextProvider; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.data.repository.query.ResultProcessor; -import org.springframework.data.repository.query.ReturnedType; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; @@ -140,11 +137,8 @@ abstract class AbstractStringBasedJpaQuery extends AbstractJpaQuery { return em.createQuery(queryString); } - ResultProcessor resultFactory = getQueryMethod().getResultProcessor(); - ReturnedType returnedType = resultFactory.getReturnedType(); - - return returnedType.isProjecting() && !getMetamodel().isJpaManaged(returnedType.getReturnedType()) - ? em.createQuery(queryString, Tuple.class) - : em.createQuery(queryString); + return getTypeToRead() // + . map(it -> em.createQuery(queryString, it)) // + .orElseGet(() -> em.createQuery(queryString)); } } diff --git a/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java b/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java index f27f13242..f32907c5e 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/NamedQuery.java @@ -145,7 +145,12 @@ final class NamedQuery extends AbstractJpaQuery { @Override protected Query doCreateQuery(Object[] values) { - Query query = getEntityManager().createNamedQuery(queryName); + EntityManager em = getEntityManager(); + + Query query = getTypeToRead() // + . map(it -> em.createNamedQuery(queryName, it)) // + .orElseGet(() -> em.createNamedQuery(queryName)); + return parameterBinder.get().bindAndPrepare(query, values); } 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 4278ff121..a5bcfa231 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 @@ -37,6 +37,7 @@ import javax.persistence.NamedEntityGraph; import javax.persistence.NamedEntityGraphs; import javax.persistence.NamedNativeQueries; import javax.persistence.NamedNativeQuery; +import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.NamedStoredProcedureQueries; import javax.persistence.NamedStoredProcedureQuery; @@ -80,7 +81,11 @@ import javax.persistence.TemporalType; @NamedAttributeNode(value = "colleagues", subgraph = "User.colleaguesOfColleagues") }), @NamedSubgraph(name = "User.colleaguesOfColleagues", attributeNodes = { @NamedAttributeNode("roles"), }) }) }) -@NamedQuery(name = "User.findByEmailAddress", query = "SELECT u FROM User u WHERE u.emailAddress = ?1") +@NamedQueries({ // + @NamedQuery(name = "User.findByEmailAddress", // + query = "SELECT u FROM User u WHERE u.emailAddress = ?1"), // + @NamedQuery(name = "User.findByNamedQueryWithAliasInInvertedOrder", // + query = "SELECT u.lastname AS lastname, u.firstname AS firstname FROM User u ORDER BY u.lastname ASC") }) @NamedStoredProcedureQueries({ // @NamedStoredProcedureQuery(name = "User.plus1", procedureName = "plus1inout", parameters = { @StoredProcedureParameter(mode = ParameterMode.IN, name = "arg", type = Integer.class), diff --git a/src/test/java/org/springframework/data/jpa/infrastructure/EclipseLinkMetamodelIntegrationTests.java b/src/test/java/org/springframework/data/jpa/infrastructure/EclipseLinkMetamodelIntegrationTests.java index 44d03c1e0..4888198ce 100644 --- a/src/test/java/org/springframework/data/jpa/infrastructure/EclipseLinkMetamodelIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/infrastructure/EclipseLinkMetamodelIntegrationTests.java @@ -50,4 +50,12 @@ public class EclipseLinkMetamodelIntegrationTests extends MetamodelIntegrationTe @Ignore @Override public void doesNotExposeAliasForTupleIfNoneDefined() {} + + /** + * TODO: Remove, once https://bugs.eclipse.org/bugs/show_bug.cgi?id=289141 is fixed. + */ + @Test + @Ignore + @Override + public void returnsAliasesInTuple() {} } diff --git a/src/test/java/org/springframework/data/jpa/infrastructure/MetamodelIntegrationTests.java b/src/test/java/org/springframework/data/jpa/infrastructure/MetamodelIntegrationTests.java index ec4ddb9b7..384ddfc96 100644 --- a/src/test/java/org/springframework/data/jpa/infrastructure/MetamodelIntegrationTests.java +++ b/src/test/java/org/springframework/data/jpa/infrastructure/MetamodelIntegrationTests.java @@ -15,8 +15,9 @@ */ package org.springframework.data.jpa.infrastructure; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; +import static org.junit.Assert.assertThat; import java.util.List; @@ -86,6 +87,7 @@ public abstract class MetamodelIntegrationTests { public void doesNotExposeAliasForTupleIfNoneDefined() { User user = new User(); + user.setFirstname("Dave"); user.setEmailAddress("email"); @@ -99,4 +101,25 @@ public abstract class MetamodelIntegrationTests { assertThat(elements, hasSize(1)); assertThat(elements.get(0).getAlias(), is(nullValue())); } + + @Test + @Transactional + public void returnsAliasesInTuple() { + + User user = new User(); + user.setFirstname("Dave"); + user.setLastname("Matthews"); + user.setEmailAddress("email"); + + em.persist(user); + + TypedQuery query = em.createQuery( + "SELECT u.lastname AS lastname, u.firstname AS firstname FROM User u ORDER BY u.lastname ASC", Tuple.class); + + List resultList = query.getResultList(); + List> elements = resultList.get(0).getElements(); + + assertThat(elements).hasSize(2); + assertThat(elements).extracting(TupleElement::getAlias).contains("firstname", "lastname"); + } } diff --git a/src/test/java/org/springframework/data/jpa/repository/EclipseLinkNamespaceUserRepositoryTests.java b/src/test/java/org/springframework/data/jpa/repository/EclipseLinkNamespaceUserRepositoryTests.java index 9e217b0e5..3858c73ad 100644 --- a/src/test/java/org/springframework/data/jpa/repository/EclipseLinkNamespaceUserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/EclipseLinkNamespaceUserRepositoryTests.java @@ -101,7 +101,13 @@ public class EclipseLinkNamespaceUserRepositoryTests extends NamespaceUserReposi @Ignore @Override @Test // DATAJPA-1248 - public void supportsProjectionsWithNativeQueriesAndCamelCaseProperty() { - super.supportsProjectionsWithNativeQueriesAndCamelCaseProperty(); - } + public void supportsProjectionsWithNativeQueriesAndCamelCaseProperty() {} + + /** + * TODO: Remove, once https://bugs.eclipse.org/bugs/show_bug.cgi?id=289141 is fixed. + */ + @Ignore + @Override + @Test + public void bindsNativeQueryResultsToProjectionByName() {} } 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 3ae4697e1..f41bb7f09 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -2160,6 +2160,19 @@ public class UserRepositoryTests { softly.assertAll(); } + @Test // DATAJPA-1273 + public void bindsNativeQueryResultsToProjectionByName() { + + flushTestUsers(); + + List result = repository.findByNamedQueryWithAliasInInvertedOrder(); + + assertThat(result).element(0).satisfies(it -> { + assertThat(it.getFirstname()).isEqualTo("Joachim"); + assertThat(it.getLastname()).isEqualTo("Arrasz"); + }); + } + private Page executeSpecWithSort(Sort sort) { flushTestUsers(); 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 3c6febbfe..d847f6dc9 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 @@ -536,6 +536,9 @@ public interface UserRepository @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); + // DATAJPA-1273 + List findByNamedQueryWithAliasInInvertedOrder(); + interface RolesAndFirstname { String getFirstname();