From 05fb4436fd6a2b6173ef0316c93b34a89d6f9d9c Mon Sep 17 00:00:00 2001 From: Lorenzo Dee Date: Fri, 2 Oct 2020 17:36:52 +0800 Subject: [PATCH 1/3] Initial attempt to support projections on query methods that accept a Specification (JpaSpecificationExecutor) --- .../repository/JpaSpecificationExecutor.java | 41 ++++ .../data/jpa/repository/query/QueryUtils.java | 4 +- .../support/JpaRepositoryFactory.java | 21 ++ .../support/JpaRepositoryImplementation.java | 17 ++ .../support/SimpleJpaRepository.java | 228 ++++++++++++++++++ .../data/jpa/util/TupleConverter.java | 174 +++++++++++++ .../jpa/repository/UserRepositoryTests.java | 58 +++++ 7 files changed, 541 insertions(+), 2 deletions(-) create mode 100644 src/main/java/org/springframework/data/jpa/util/TupleConverter.java diff --git a/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java b/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java index 6ab0fa5fa2..cb6b78e735 100644 --- a/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java +++ b/src/main/java/org/springframework/data/jpa/repository/JpaSpecificationExecutor.java @@ -29,6 +29,7 @@ * * @author Oliver Gierke * @author Christoph Strobl + * @author Lorenzo Dee */ public interface JpaSpecificationExecutor { @@ -41,6 +42,17 @@ public interface JpaSpecificationExecutor { */ Optional findOne(@Nullable Specification spec); + /** + * Returns a projection of a single entity matching the given {@link Specification}, or + * {@link Optional#empty()} if none found. + * + * @param spec can be {@literal null}. + * @param projectionType must not be {@literal null}. + * @return never {@literal null}. + * @throws org.springframework.dao.IncorrectResultSizeDataAccessException if more than one entity found. + */ +

Optional

findOne(@Nullable Specification spec, Class

projectionType); + /** * Returns all entities matching the given {@link Specification}. * @@ -49,6 +61,15 @@ public interface JpaSpecificationExecutor { */ List findAll(@Nullable Specification spec); + /** + * Returns projections of all entities matching the given {@link Specification}. + * + * @param spec can be {@literal null}. + * @param projectionType must not be {@literal null}. + * @return never {@literal null}. + */ +

List

findAll(@Nullable Specification spec, Class

projectionType); + /** * Returns a {@link Page} of entities matching the given {@link Specification}. * @@ -58,6 +79,16 @@ public interface JpaSpecificationExecutor { */ Page findAll(@Nullable Specification spec, Pageable pageable); + /** + * Returns a {@link Page} of projections matching the given {@link Specification}. + * + * @param spec can be {@literal null}. + * @param pageable must not be {@literal null}. + * @param projectionType must not be {@literal null}. + * @return never {@literal null}. + */ +

Page

findAll(@Nullable Specification spec, Pageable pageable, Class

projectionType); + /** * Returns all entities matching the given {@link Specification} and {@link Sort}. * @@ -67,6 +98,16 @@ public interface JpaSpecificationExecutor { */ List findAll(@Nullable Specification spec, Sort sort); + /** + * Returns projections of all entities matching the given {@link Specification} and {@link Sort}. + * + * @param spec can be {@literal null}. + * @param sort must not be {@literal null}. + * @param projectionType must not be {@literal null}. + * @return never {@literal null}. + */ +

List

findAll(@Nullable Specification spec, Sort sort, Class

projectionType); + /** * Returns the number of instances that the given {@link Specification} will return. * diff --git a/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java b/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java index f59aeb5857..eae44830f3 100644 --- a/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java +++ b/src/main/java/org/springframework/data/jpa/repository/query/QueryUtils.java @@ -615,12 +615,12 @@ private static javax.persistence.criteria.Order toJpaOrder(Order order, From Expression toExpressionRecursively(From from, PropertyPath property) { + public static Expression toExpressionRecursively(From from, PropertyPath property) { return toExpressionRecursively(from, property, false); } @SuppressWarnings("unchecked") - static Expression toExpressionRecursively(From from, PropertyPath property, boolean isForSelection) { + public static Expression toExpressionRecursively(From from, PropertyPath property, boolean isForSelection) { Bindable propertyPathModel; Bindable model = from.getModel(); diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java index a9535f2d5f..e8078f3273 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryFactory.java @@ -27,12 +27,14 @@ import javax.persistence.EntityManager; import javax.persistence.Tuple; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.jpa.projection.CollectionAwareProjectionFactory; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.provider.QueryExtractor; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.query.AbstractJpaQuery; import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.jpa.repository.query.JpaQueryLookupStrategy; @@ -75,6 +77,9 @@ public class JpaRepositoryFactory extends RepositoryFactorySupport { private EntityPathResolver entityPathResolver; private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT; + private ClassLoader classLoader; + private BeanFactory beanFactory; + /** * Creates a new {@link JpaRepositoryFactory}. * @@ -110,9 +115,20 @@ public JpaRepositoryFactory(EntityManager entityManager) { public void setBeanClassLoader(ClassLoader classLoader) { super.setBeanClassLoader(classLoader); + this.classLoader = classLoader == null ? org.springframework.util.ClassUtils.getDefaultClassLoader() : classLoader; this.crudMethodMetadataPostProcessor.setBeanClassLoader(classLoader); } + /* + * (non-Javadoc) + * @see org.springframework.data.repository.core.support.RepositoryFactorySupport#setBeanFactory(org.springframework.beans.factory.BeanFactory) + */ + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + super.setBeanFactory(beanFactory); + this.beanFactory = beanFactory; + } + /** * Configures the {@link EntityPathResolver} to be used. Defaults to {@link SimpleEntityPathResolver#INSTANCE}. * @@ -145,6 +161,11 @@ public void setEscapeCharacter(EscapeCharacter escapeCharacter) { repository.setRepositoryMethodMetadata(crudMethodMetadataPostProcessor.getCrudMethodMetadata()); repository.setEscapeCharacter(escapeCharacter); + if (repository instanceof JpaSpecificationExecutor) { + repository.setProjectionFactory(getProjectionFactory(classLoader, beanFactory)); + repository.setRepositoryInformation(information); + } + return repository; } diff --git a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryImplementation.java b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryImplementation.java index 9dcc321c47..31209df972 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryImplementation.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/JpaRepositoryImplementation.java @@ -18,7 +18,9 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; import org.springframework.data.jpa.repository.query.EscapeCharacter; +import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.core.RepositoryInformation; /** * SPI interface to be implemented by {@link JpaRepository} implementations. @@ -26,6 +28,7 @@ * @author Oliver Gierke * @author Stefan Fussenegger * @author Jens Schauder + * @author Lorenzo Dee */ @NoRepositoryBean public interface JpaRepositoryImplementation extends JpaRepository, JpaSpecificationExecutor { @@ -45,4 +48,18 @@ public interface JpaRepositoryImplementation extends JpaRepository default void setEscapeCharacter(EscapeCharacter escapeCharacter) { } + + /** + * Configures the {@link ProjectionFactory} to be used with the repository. + * + * @param projectionFactory must not be {@literal null}. + */ + void setProjectionFactory(ProjectionFactory projectionFactory); + + /** + * Configures the {@link RepositoryInformation} to be used with the repository. + * + * @param information must not be {@literal null}. + */ + void setRepositoryInformation(RepositoryInformation information); } diff --git a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index 6ad9f11e55..f2a68fd071 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -17,19 +17,25 @@ import static org.springframework.data.jpa.repository.query.QueryUtils.*; +import java.beans.PropertyDescriptor; +import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; +import java.util.Set; import javax.persistence.EntityManager; import javax.persistence.LockModeType; import javax.persistence.NoResultException; import javax.persistence.Parameter; import javax.persistence.Query; +import javax.persistence.Tuple; import javax.persistence.TypedQuery; import javax.persistence.criteria.CriteriaBuilder; import javax.persistence.criteria.CriteriaQuery; @@ -38,6 +44,7 @@ import javax.persistence.criteria.Path; import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; +import javax.persistence.criteria.Selection; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.domain.Example; @@ -52,12 +59,26 @@ import org.springframework.data.jpa.repository.query.EscapeCharacter; import org.springframework.data.jpa.repository.query.QueryUtils; import org.springframework.data.jpa.repository.support.QueryHints.NoHints; +import org.springframework.data.jpa.util.TupleConverter; +import org.springframework.data.mapping.PreferredConstructor; +import org.springframework.data.mapping.PropertyPath; +import org.springframework.data.mapping.model.PreferredConstructorDiscoverer; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.ProjectionInformation; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.query.ParameterAccessor; +import org.springframework.data.repository.query.ParametersParameterAccessor; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.support.PageableExecutionUtils; +import org.springframework.data.util.Pair; import org.springframework.data.util.ProxyUtils; import org.springframework.lang.Nullable; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; +import org.springframework.util.ConcurrentReferenceHashMap; /** * Default implementation of the {@link org.springframework.data.repository.CrudRepository} interface. This will offer @@ -72,6 +93,7 @@ * @author Jens Schauder * @author David Madden * @author Moritz Becker + * @author Lorenzo Dee * @param the type of the entity to handle * @param the type of the entity's identifier */ @@ -88,6 +110,13 @@ public class SimpleJpaRepository implements JpaRepositoryImplementation queriesCache = new ConcurrentReferenceHashMap<>(8); + private final Map>, ParameterAccessor> accessorsCache = new ConcurrentReferenceHashMap<>(32); + private final Map>, ResultProcessor> resultProcessorsCache = new ConcurrentReferenceHashMap<>(32); + private static Collection toCollection(Iterable ts) { if (ts instanceof Collection) { @@ -142,6 +171,16 @@ public void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata) { public void setEscapeCharacter(EscapeCharacter escapeCharacter) { this.escapeCharacter = escapeCharacter; } + + @Override + public void setProjectionFactory(ProjectionFactory projectionFactory) { + this.projectionFactory = projectionFactory; + } + + @Override + public void setRepositoryInformation(RepositoryInformation information) { + this.information = information; + } @Nullable protected CrudMethodMetadata getRepositoryMethodMetadata() { @@ -424,6 +463,28 @@ public Optional findOne(@Nullable Specification spec) { } } + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findOne(org.springframework.data.jpa.domain.Specification, java.lang.Class) + */ + @Override + public

Optional

findOne(@Nullable Specification spec, Class

projectionType) { + /* + if (metadata == null || projectionFactory == null || information == null) { + // Should we fallback on to using domain class instead of projection type? + return (Optional

) findOne(spec); + } + */ + try { + Tuple result = getTupleQuery( + spec, getDomainClass(), Sort.unsorted(), projectionType).getSingleResult(); + ResultProcessor resultProcessor = getResultProcessor(projectionType); + return Optional.of(resultProcessor.processResult(result, new TupleConverter(projectionType))); + } catch (NoResultException e) { + return Optional.empty(); + } + } + /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll(org.springframework.data.jpa.domain.Specification) @@ -433,6 +494,15 @@ public List findAll(@Nullable Specification spec) { return getQuery(spec, Sort.unsorted()).getResultList(); } + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll(org.springframework.data.jpa.domain.Specification, java.lang.Class) + */ + @Override + public

List

findAll(@Nullable Specification spec, Class

projectionType) { + return findAll(spec, Sort.unsorted(), projectionType); + } + /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll(org.springframework.data.jpa.domain.Specification, org.springframework.data.domain.Pageable) @@ -445,6 +515,21 @@ public Page findAll(@Nullable Specification spec, Pageable pageable) { : readPage(query, getDomainClass(), pageable, spec); } + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll(org.springframework.data.jpa.domain.Specification, org.springframework.data.domain.Pageable, java.lang.Class) + */ + @Override + public

Page

findAll(@Nullable Specification spec, Pageable pageable, Class

projectionType) { + + Sort sort = pageable.isPaged() ? pageable.getSort() : Sort.unsorted(); + TypedQuery query = getTupleQuery( + spec, getDomainClass(), sort, projectionType); + return isUnpaged(pageable) + ? new PageImpl

(getResultProcessor(projectionType).processResult(query.getResultList(), new TupleConverter(projectionType))) + : readPage(query, getDomainClass(), pageable, spec, projectionType); + } + /* * (non-Javadoc) * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll(org.springframework.data.jpa.domain.Specification, org.springframework.data.domain.Sort) @@ -454,6 +539,18 @@ public List findAll(@Nullable Specification spec, Sort sort) { return getQuery(spec, sort).getResultList(); } + /* + * (non-Javadoc) + * @see org.springframework.data.jpa.repository.JpaSpecificationExecutor#findAll(org.springframework.data.jpa.domain.Specification, org.springframework.data.domain.Sort, java.lang.Class) + */ + @Override + public

List

findAll(@Nullable Specification spec, Sort sort, Class

projectionType) { + List resultList = getTupleQuery( + spec, getDomainClass(), sort, projectionType).getResultList(); + ResultProcessor resultProcessor = getResultProcessor(projectionType); + return resultProcessor.processResult(resultList, new TupleConverter(projectionType)); + } + /* * (non-Javadoc) * @see org.springframework.data.repository.query.QueryByExampleExecutor#findOne(org.springframework.data.domain.Example) @@ -638,6 +735,31 @@ protected Page readPage(TypedQuery query, final Class dom () -> executeCountQuery(getCountQuery(spec, domainClass))); } + /** + * Reads the given {@link TypedQuery} into a {@link Page} applying the given {@link Pageable} and + * {@link Specification} and projection. + * + * @param query must not be {@literal null}. + * @param domainClass must not be {@literal null}. + * @param spec can be {@literal null}. + * @param pageable can be {@literal null}. + * @param projectionType must not be {@literal null}. + * @return + */ + protected Page

readPage(TypedQuery query, final Class domainClass, Pageable pageable, + @Nullable Specification spec, Class

projectionType) { + + if (pageable.isPaged()) { + query.setFirstResult((int) pageable.getOffset()); + query.setMaxResults(pageable.getPageSize()); + } + ResultProcessor resultProcessor = getResultProcessor(projectionType); + Page tuplesPage = PageableExecutionUtils.getPage( + query.getResultList(), pageable, () -> executeCountQuery(getCountQuery(spec, domainClass))); + // Page --> Page

+ return resultProcessor.processResult(tuplesPage, new TupleConverter(projectionType)); + } + /** * Creates a new {@link TypedQuery} from the given {@link Specification}. * @@ -700,6 +822,91 @@ protected TypedQuery getQuery(@Nullable Specification spec, return applyRepositoryMethodMetadata(em.createQuery(query)); } + /** + * Creates a {@link TypedQuery} for the given {@link Specification}, {@link Sort}, and projection. + * + * @param spec can be {@literal null}. + * @param domainClass must not be {@literal null}. + * @param sort must not be {@literal null}. + * @param projectionType must not be {@literal null}. + * @return + */ + protected TypedQuery getTupleQuery(@Nullable Specification spec, Class domainClass, Sort sort, Class projectionType) { + + CriteriaBuilder builder = em.getCriteriaBuilder(); + CriteriaQuery query = builder.createTupleQuery(); + + Root root = applySpecificationToCriteria(spec, domainClass, query); + + ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(projectionType); + List properties = null; + if (projectionType.isInterface()) { + properties = new ArrayList<>(); + for (PropertyDescriptor descriptor : projectionInformation.getInputProperties()) { + if (!properties.contains(descriptor.getName())) { + properties.add(descriptor.getName()); + } + } + } + else { + properties = detectConstructorParameterNames(projectionType); + } + + List> selections = new ArrayList<>(); + for (String property : properties) { + PropertyPath path = PropertyPath.from(property, projectionType); + selections.add(toExpressionRecursively(root, path, true).alias(property)); + } + query.multiselect(selections); + + if (sort.isSorted()) { + query.orderBy(toOrders(sort, root, builder)); + } + + return applyRepositoryMethodMetadata(em.createQuery(query)); + } + + private List detectConstructorParameterNames(Class type) { + + if (!isDto(type)) { + return Collections.emptyList(); + } + + PreferredConstructor constructor = PreferredConstructorDiscoverer.discover(type); + + if (constructor == null) { + return Collections.emptyList(); + } + + List properties = new ArrayList<>(constructor.getConstructor().getParameterCount()); + + for (PreferredConstructor.Parameter parameter : constructor.getParameters()) { + properties.add(parameter.getName()); + } + + return properties; + } + + private static final Set> VOID_TYPES = new HashSet<>(Arrays.asList(Void.class, void.class)); + + private boolean isDto(Class type) { + return !Object.class.equals(type) && // + !type.isEnum() && // + !isDomainSubtype(type) && // + !isPrimitiveOrWrapper(type) && // + !Number.class.isAssignableFrom(type) && // + !VOID_TYPES.contains(type) && // + !type.getPackage().getName().startsWith("java."); + } + + private boolean isDomainSubtype(Class type) { + return getDomainClass().equals(type) && getDomainClass().isAssignableFrom(type); + } + + private boolean isPrimitiveOrWrapper(Class type) { + return ClassUtils.isPrimitiveOrWrapper(type); + } + /** * Creates a new count query for the given {@link Specification}. * @@ -789,6 +996,27 @@ private void applyQueryHints(Query query) { } } + private

ResultProcessor getResultProcessor(Class

projectionType) { + + Method method = metadata.getMethod(); + QueryMethod queryMethod = queriesCache.computeIfAbsent( + method, + key -> new QueryMethod(method, information, projectionFactory)); + int numberOfParameters = queryMethod.getParameters().getNumberOfParameters(); + Object values[] = new Object[numberOfParameters]; + ParameterAccessor accessor = accessorsCache.computeIfAbsent( + Pair.of(method, projectionType), + key -> new ParametersParameterAccessor(queryMethod.getParameters(), values) { + @Override + public Class findDynamicProjection() { + return projectionType; + } + }); + return resultProcessorsCache.computeIfAbsent( + Pair.of(method, projectionType), + key -> queryMethod.getResultProcessor().withDynamicProjection(accessor)); + } + /** * Executes a count query and transparently sums up all values returned. * diff --git a/src/main/java/org/springframework/data/jpa/util/TupleConverter.java b/src/main/java/org/springframework/data/jpa/util/TupleConverter.java new file mode 100644 index 0000000000..db6f7e00b1 --- /dev/null +++ b/src/main/java/org/springframework/data/jpa/util/TupleConverter.java @@ -0,0 +1,174 @@ +package org.springframework.data.jpa.util; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.persistence.Tuple; +import javax.persistence.TupleElement; + +import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +public class TupleConverter implements Converter { + + private final Class type; + + /** + * Creates a new {@link TupleConverter} for the given {@link Class}. + * + * @param type must not be {@literal null}. + */ + public TupleConverter(Class type) { + + Assert.notNull(type, "Returned type must not be null!"); + + this.type = type; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + if (!(source instanceof Tuple)) { + return source; + } + + Tuple tuple = (Tuple) source; + List> elements = tuple.getElements(); + + if (elements.size() == 1) { + + Object value = tuple.get(elements.get(0)); + + if (type.isInstance(value) || value == null) { + return value; + } + } + + return new TupleBackedMap(tuple); + } + + /** + * A {@link Map} implementation which delegates all calls to a {@link Tuple}. Depending on the provided + * {@link Tuple} implementation it might return the same value for various keys of which only one will appear in the + * key/entry set. + * + * @author Jens Schauder + */ + private static class TupleBackedMap implements Map { + + private static final String UNMODIFIABLE_MESSAGE = "A TupleBackedMap cannot be modified."; + + private final Tuple tuple; + + TupleBackedMap(Tuple tuple) { + this.tuple = tuple; + } + + @Override + public int size() { + return tuple.getElements().size(); + } + + @Override + public boolean isEmpty() { + return tuple.getElements().isEmpty(); + } + + /** + * If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code false}. + * Otherwise this returns {@code true} even when the value from the backing {@code Tuple} is {@code null}. + * + * @param key the key for which to get the value from the map. + * @return wether the key is an element of the backing tuple. + */ + @Override + public boolean containsKey(Object key) { + + try { + tuple.get((String) key); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + @Override + public boolean containsValue(Object value) { + return Arrays.asList(tuple.toArray()).contains(value); + } + + /** + * If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code null}. + * Otherwise the value from the backing {@code Tuple} is returned, which also might be {@code null}. + * + * @param key the key for which to get the value from the map. + * @return the value of the backing {@link Tuple} for that key or {@code null}. + */ + @Override + @Nullable + public Object get(Object key) { + + if (!(key instanceof String)) { + return null; + } + + try { + return tuple.get((String) key); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Override + public Object put(String key, Object value) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public Object remove(Object key) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public void putAll(Map m) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public Set keySet() { + + return tuple.getElements().stream() // + .map(TupleElement::getAlias) // + .collect(Collectors.toSet()); + } + + @Override + public Collection values() { + return Arrays.asList(tuple.toArray()); + } + + @Override + public Set> entrySet() { + + return tuple.getElements().stream() // + .map(e -> new HashMap.SimpleEntry(e.getAlias(), tuple.get(e))) // + .collect(Collectors.toSet()); + } + } + +} 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 9855fbea6a..5c2d4f7eb7 100644 --- a/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java +++ b/src/test/java/org/springframework/data/jpa/repository/UserRepositoryTests.java @@ -90,6 +90,7 @@ * @author Kevin Peters * @author Jens Schauder * @author Andrey Kovalev + * @author Lorenzo Dee */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:application-context.xml") @@ -2280,6 +2281,63 @@ public void findByElementCollectionInAttributeIgnoreCaseWithNulls() { assertThat(result).containsOnly(firstUser); } + @Test // DATAJPA-1033 + public void executesSingleEntitySpecificationAndReturnsProjection() throws Exception { + + flushTestUsers(); + + NameOnly result = repository.findOne(userHasFirstname("Oliver"), NameOnly.class).get(); + + assertThat(result.getFirstname()).isEqualTo(firstUser.getFirstname()); + assertThat(result.getLastname()).isEqualTo(firstUser.getLastname()); + } + + @Test // DATAJPA-1033 + public void executesSpecificationAndReturnsProjection() { + + flushTestUsers(); + assertThat(repository.findAll(where(userHasFirstname("Oliver")), NameOnly.class)).hasSize(1); + } + + @Test(expected = IncorrectResultSizeDataAccessException.class) // DATAJPA-1033 + public void throwsExceptionForUnderSpecifiedSingleEntitySpecificationAndProjection() { + + flushTestUsers(); + repository.findOne(userHasFirstnameLike("e"), NameOnly.class); + } + + @Test // DATAJPA-1033 + public void executesCombinedSpecificationsAndReturnsProjection() { + + flushTestUsers(); + Specification spec = userHasFirstname("Oliver").or(userHasLastname("Arrasz")); + assertThat(repository.findAll(spec, NameOnly.class)).hasSize(2); + } + + @Test // DATAJPA-1033 + public void executesNegatingSpecificationAndReturnsProjection() { + + flushTestUsers(); + Specification spec = not(userHasFirstname("Oliver")).and(userHasLastname("Arrasz")); + + List result = repository.findAll(spec, NameOnly.class); + assertThat(result).hasSize(1); + assertThat(result.get(0).getFirstname()).isEqualTo(secondUser.getFirstname()); + assertThat(result.get(0).getLastname()).isEqualTo(secondUser.getLastname()); + } + + @Test // DATAJPA-1033 + public void executesCombinedSpecificationsWithPageableAndReturnsProjection() { + + flushTestUsers(); + Specification spec = userHasFirstname("Oliver").or(userHasLastname("Arrasz")); + + Page users = repository.findAll(spec, PageRequest.of(0, 1), NameOnly.class); + assertThat(users.getSize()).isEqualTo(1); + assertThat(users.hasPrevious()).isFalse(); + assertThat(users.getTotalElements()).isEqualTo(2L); + } + private Page executeSpecWithSort(Sort sort) { flushTestUsers(); From 120264a79fc1340a7e8a06508d652354a92c28e1 Mon Sep 17 00:00:00 2001 From: Lorenzo Dee Date: Fri, 2 Oct 2020 18:34:07 +0800 Subject: [PATCH 2/3] Refactored AbstractJpaQuery to use the same TupleConverter used in SimpleJpaRepository --- .../repository/query/AbstractJpaQuery.java | 167 +-------- .../data/jpa/util/TupleConverter.java | 321 +++++++++--------- .../TupleConverterUnitTests.java | 24 +- 3 files changed, 173 insertions(+), 339 deletions(-) rename src/test/java/org/springframework/data/jpa/{repository/query => util}/TupleConverterUnitTests.java (79%) 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 556e866b8a..8299f3d89a 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 @@ -15,23 +15,16 @@ */ package org.springframework.data.jpa.repository.query; -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; import javax.persistence.EntityManager; import javax.persistence.LockModeType; import javax.persistence.Query; import javax.persistence.QueryHint; import javax.persistence.Tuple; -import javax.persistence.TupleElement; import javax.persistence.TypedQuery; -import org.springframework.core.convert.converter.Converter; import org.springframework.data.jpa.provider.PersistenceProvider; import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.query.JpaQueryExecution.CollectionExecution; @@ -42,6 +35,7 @@ import org.springframework.data.jpa.repository.query.JpaQueryExecution.SlicedExecution; import org.springframework.data.jpa.repository.query.JpaQueryExecution.StreamExecution; import org.springframework.data.jpa.util.JpaMetamodel; +import org.springframework.data.jpa.util.TupleConverter; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; @@ -59,6 +53,7 @@ * @author Nicolas Cirigliano * @author Jens Schauder * @author Сергей Цыпанов + * @author Lorenzo Dee */ public abstract class AbstractJpaQuery implements RepositoryQuery { @@ -154,7 +149,7 @@ private Object doExecute(JpaQueryExecution execution, Object[] values) { Object result = execution.execute(this, accessor); ResultProcessor withDynamicProjection = method.getResultProcessor().withDynamicProjection(accessor); - return withDynamicProjection.processResult(result, new TupleConverter(withDynamicProjection.getReturnedType())); + return withDynamicProjection.processResult(result, new TupleConverter(withDynamicProjection.getReturnedType().getReturnedType())); } protected JpaQueryExecution getExecution() { @@ -289,160 +284,4 @@ protected Class getTypeToRead(ReturnedType returnedType) { */ protected abstract Query doCreateCountQuery(JpaParametersParameterAccessor accessor); - static class TupleConverter implements Converter { - - private final ReturnedType type; - - /** - * Creates a new {@link TupleConverter} for the given {@link ReturnedType}. - * - * @param type must not be {@literal null}. - */ - public TupleConverter(ReturnedType type) { - - Assert.notNull(type, "Returned type must not be null!"); - - this.type = type; - } - - /* - * (non-Javadoc) - * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) - */ - @Override - public Object convert(Object source) { - - if (!(source instanceof Tuple)) { - return source; - } - - Tuple tuple = (Tuple) source; - List> elements = tuple.getElements(); - - if (elements.size() == 1) { - - Object value = tuple.get(elements.get(0)); - - if (type.isInstance(value) || value == null) { - return value; - } - } - - return new TupleBackedMap(tuple); - } - - /** - * A {@link Map} implementation which delegates all calls to a {@link Tuple}. Depending on the provided - * {@link Tuple} implementation it might return the same value for various keys of which only one will appear in the - * key/entry set. - * - * @author Jens Schauder - */ - private static class TupleBackedMap implements Map { - - private static final String UNMODIFIABLE_MESSAGE = "A TupleBackedMap cannot be modified."; - - private final Tuple tuple; - - TupleBackedMap(Tuple tuple) { - this.tuple = tuple; - } - - @Override - public int size() { - return tuple.getElements().size(); - } - - @Override - public boolean isEmpty() { - return tuple.getElements().isEmpty(); - } - - /** - * If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code false}. - * Otherwise this returns {@code true} even when the value from the backing {@code Tuple} is {@code null}. - * - * @param key the key for which to get the value from the map. - * @return whether the key is an element of the backing tuple. - */ - @Override - public boolean containsKey(Object key) { - - try { - tuple.get((String) key); - return true; - } catch (IllegalArgumentException e) { - return false; - } - } - - @Override - public boolean containsValue(Object value) { - return Arrays.asList(tuple.toArray()).contains(value); - } - - /** - * If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code null}. - * Otherwise the value from the backing {@code Tuple} is returned, which also might be {@code null}. - * - * @param key the key for which to get the value from the map. - * @return the value of the backing {@link Tuple} for that key or {@code null}. - */ - @Override - @Nullable - public Object get(Object key) { - - if (!(key instanceof String)) { - return null; - } - - try { - return tuple.get((String) key); - } catch (IllegalArgumentException e) { - return null; - } - } - - @Override - public Object put(String key, Object value) { - throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); - } - - @Override - public Object remove(Object key) { - throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); - } - - @Override - public void putAll(Map m) { - throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); - } - - @Override - public void clear() { - throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); - } - - @Override - public Set keySet() { - - return tuple.getElements().stream() // - .map(TupleElement::getAlias) // - .collect(Collectors.toSet()); - } - - @Override - public Collection values() { - return Arrays.asList(tuple.toArray()); - } - - @Override - public Set> entrySet() { - - return tuple.getElements().stream() // - .map(e -> new HashMap.SimpleEntry(e.getAlias(), tuple.get(e))) // - .collect(Collectors.toSet()); - } - } - } } diff --git a/src/main/java/org/springframework/data/jpa/util/TupleConverter.java b/src/main/java/org/springframework/data/jpa/util/TupleConverter.java index db6f7e00b1..3ae81cae6b 100644 --- a/src/main/java/org/springframework/data/jpa/util/TupleConverter.java +++ b/src/main/java/org/springframework/data/jpa/util/TupleConverter.java @@ -1,3 +1,18 @@ +/* + * Copyright 2008-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ package org.springframework.data.jpa.util; import java.util.Arrays; @@ -17,158 +32,156 @@ public class TupleConverter implements Converter { - private final Class type; - - /** - * Creates a new {@link TupleConverter} for the given {@link Class}. - * - * @param type must not be {@literal null}. - */ - public TupleConverter(Class type) { - - Assert.notNull(type, "Returned type must not be null!"); - - this.type = type; - } - - /* - * (non-Javadoc) - * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) - */ - @Override - public Object convert(Object source) { - - if (!(source instanceof Tuple)) { - return source; - } - - Tuple tuple = (Tuple) source; - List> elements = tuple.getElements(); - - if (elements.size() == 1) { - - Object value = tuple.get(elements.get(0)); - - if (type.isInstance(value) || value == null) { - return value; - } - } - - return new TupleBackedMap(tuple); - } - - /** - * A {@link Map} implementation which delegates all calls to a {@link Tuple}. Depending on the provided - * {@link Tuple} implementation it might return the same value for various keys of which only one will appear in the - * key/entry set. - * - * @author Jens Schauder - */ - private static class TupleBackedMap implements Map { - - private static final String UNMODIFIABLE_MESSAGE = "A TupleBackedMap cannot be modified."; - - private final Tuple tuple; - - TupleBackedMap(Tuple tuple) { - this.tuple = tuple; - } - - @Override - public int size() { - return tuple.getElements().size(); - } - - @Override - public boolean isEmpty() { - return tuple.getElements().isEmpty(); - } - - /** - * If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code false}. - * Otherwise this returns {@code true} even when the value from the backing {@code Tuple} is {@code null}. - * - * @param key the key for which to get the value from the map. - * @return wether the key is an element of the backing tuple. - */ - @Override - public boolean containsKey(Object key) { - - try { - tuple.get((String) key); - return true; - } catch (IllegalArgumentException e) { - return false; - } - } - - @Override - public boolean containsValue(Object value) { - return Arrays.asList(tuple.toArray()).contains(value); - } - - /** - * If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code null}. - * Otherwise the value from the backing {@code Tuple} is returned, which also might be {@code null}. - * - * @param key the key for which to get the value from the map. - * @return the value of the backing {@link Tuple} for that key or {@code null}. - */ - @Override - @Nullable - public Object get(Object key) { - - if (!(key instanceof String)) { - return null; - } - - try { - return tuple.get((String) key); - } catch (IllegalArgumentException e) { - return null; - } - } - - @Override - public Object put(String key, Object value) { - throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); - } - - @Override - public Object remove(Object key) { - throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); - } - - @Override - public void putAll(Map m) { - throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); - } - - @Override - public void clear() { - throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); - } - - @Override - public Set keySet() { - - return tuple.getElements().stream() // - .map(TupleElement::getAlias) // - .collect(Collectors.toSet()); - } - - @Override - public Collection values() { - return Arrays.asList(tuple.toArray()); - } - - @Override - public Set> entrySet() { - - return tuple.getElements().stream() // - .map(e -> new HashMap.SimpleEntry(e.getAlias(), tuple.get(e))) // - .collect(Collectors.toSet()); - } - } - + private final Class type; + + /** + * Creates a new {@link TupleConverter} for the given {@link Class}. + * + * @param type must not be {@literal null}. + */ + public TupleConverter(Class type) { + + Assert.notNull(type, "Type must not be null!"); + + this.type = type; + } + + /* + * (non-Javadoc) + * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object) + */ + @Override + public Object convert(Object source) { + + if (!(source instanceof Tuple)) { + return source; + } + + Tuple tuple = (Tuple) source; + List> elements = tuple.getElements(); + + if (elements.size() == 1) { + + Object value = tuple.get(elements.get(0)); + + if (type.isInstance(value) || value == null) { + return value; + } + } + + return new TupleBackedMap(tuple); + } + + /** + * A {@link Map} implementation which delegates all calls to a {@link Tuple}. Depending on the provided + * {@link Tuple} implementation it might return the same value for various keys of which only one will appear in the + * key/entry set. + * + * @author Jens Schauder + */ + private static class TupleBackedMap implements Map { + private static final String UNMODIFIABLE_MESSAGE = "A TupleBackedMap cannot be modified."; + + private final Tuple tuple; + + public TupleBackedMap(Tuple tuple) { + this.tuple = tuple; + } + + @Override + public int size() { + return tuple.getElements().size(); + } + + @Override + public boolean isEmpty() { + return tuple.getElements().isEmpty(); + } + + /** + * If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code false}. + * Otherwise this returns {@code true} even when the value from the backing {@code Tuple} is {@code null}. + * + * @param key the key for which to get the value from the map. + * @return whether the key is an element of the backing tuple. + */ + @Override + public boolean containsKey(Object key) { + + try { + tuple.get((String) key); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + @Override + public boolean containsValue(Object value) { + return Arrays.asList(tuple.toArray()).contains(value); + } + + /** + * If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code null}. + * Otherwise the value from the backing {@code Tuple} is returned, which also might be {@code null}. + * + * @param key the key for which to get the value from the map. + * @return the value of the backing {@link Tuple} for that key or {@code null}. + */ + @Override + @Nullable + public Object get(Object key) { + + if (!(key instanceof String)) { + return null; + } + + try { + return tuple.get((String) key); + } catch (IllegalArgumentException e) { + return null; + } + } + + @Override + public Object put(String key, Object value) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public Object remove(Object key) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public void putAll(Map m) { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public void clear() { + throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE); + } + + @Override + public Set keySet() { + + return tuple.getElements().stream() // + .map(TupleElement::getAlias) // + .collect(Collectors.toSet()); + } + + @Override + public Collection values() { + return Arrays.asList(tuple.toArray()); + } + + @Override + public Set> entrySet() { + + return tuple.getElements().stream() // + .map(e -> new HashMap.SimpleEntry(e.getAlias(), tuple.get(e))) // + .collect(Collectors.toSet()); + } + } } diff --git a/src/test/java/org/springframework/data/jpa/repository/query/TupleConverterUnitTests.java b/src/test/java/org/springframework/data/jpa/util/TupleConverterUnitTests.java similarity index 79% rename from src/test/java/org/springframework/data/jpa/repository/query/TupleConverterUnitTests.java rename to src/test/java/org/springframework/data/jpa/util/TupleConverterUnitTests.java index 325cc1b9ba..ae92ce2c67 100644 --- a/src/test/java/org/springframework/data/jpa/repository/query/TupleConverterUnitTests.java +++ b/src/test/java/org/springframework/data/jpa/util/TupleConverterUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.jpa.repository.query; +package org.springframework.data.jpa.util; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; @@ -32,13 +32,6 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.data.jpa.repository.query.AbstractJpaQuery.TupleConverter; -import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.repository.CrudRepository; -import org.springframework.data.repository.core.RepositoryMetadata; -import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.repository.query.ReturnedType; /** * Unit tests for {@link TupleConverter}. @@ -52,21 +45,15 @@ public class TupleConverterUnitTests { @Mock Tuple tuple; @Mock TupleElement element; - @Mock ProjectionFactory factory; - ReturnedType type; + Class type; @Before public void setUp() throws Exception { - - RepositoryMetadata metadata = new DefaultRepositoryMetadata(SampleRepository.class); - QueryMethod method = new QueryMethod(SampleRepository.class.getMethod("someMethod"), metadata, factory); - - this.type = method.getResultProcessor().getReturnedType(); + this.type = String.class; } @Test // DATAJPA-984 - @SuppressWarnings("unchecked") public void returnsSingleTupleElementIfItMatchesExpectedType() { doReturn(Collections.singletonList(element)).when(tuple).getElements(); @@ -78,7 +65,6 @@ public void returnsSingleTupleElementIfItMatchesExpectedType() { } @Test // DATAJPA-1024 - @SuppressWarnings("unchecked") public void returnsNullForSingleElementTupleWithNullValue() { doReturn(Collections.singletonList(element)).when(tuple).getElements(); @@ -109,10 +95,6 @@ public void findsValuesForAllVariantsSupportedByTheTuple() { softly.assertAll(); } - interface SampleRepository extends CrudRepository { - String someMethod(); - } - @SuppressWarnings("unchecked") private static class MockTuple implements Tuple { From 2757fb965922fc01fc1f839a44ed4c5f14b6fa19 Mon Sep 17 00:00:00 2001 From: Lorenzo Dee Date: Mon, 5 Oct 2020 10:59:57 +0800 Subject: [PATCH 3/3] Re-use ReturnedType from ResultProcessor of QueryMethod --- .../support/SimpleJpaRepository.java | 100 +++--------------- 1 file changed, 17 insertions(+), 83 deletions(-) diff --git a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java index f2a68fd071..7ab3ca5471 100644 --- a/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java +++ b/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java @@ -17,18 +17,14 @@ import static org.springframework.data.jpa.repository.query.QueryUtils.*; -import java.beans.PropertyDescriptor; import java.lang.reflect.Method; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; -import java.util.Set; import javax.persistence.EntityManager; import javax.persistence.LockModeType; @@ -60,16 +56,14 @@ import org.springframework.data.jpa.repository.query.QueryUtils; import org.springframework.data.jpa.repository.support.QueryHints.NoHints; import org.springframework.data.jpa.util.TupleConverter; -import org.springframework.data.mapping.PreferredConstructor; import org.springframework.data.mapping.PropertyPath; -import org.springframework.data.mapping.model.PreferredConstructorDiscoverer; import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.projection.ProjectionInformation; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.repository.query.ParametersParameterAccessor; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.support.PageableExecutionUtils; import org.springframework.data.util.Pair; import org.springframework.data.util.ProxyUtils; @@ -77,7 +71,6 @@ import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; /** @@ -469,16 +462,10 @@ public Optional findOne(@Nullable Specification spec) { */ @Override public

Optional

findOne(@Nullable Specification spec, Class

projectionType) { - /* - if (metadata == null || projectionFactory == null || information == null) { - // Should we fallback on to using domain class instead of projection type? - return (Optional

) findOne(spec); - } - */ try { - Tuple result = getTupleQuery( - spec, getDomainClass(), Sort.unsorted(), projectionType).getSingleResult(); ResultProcessor resultProcessor = getResultProcessor(projectionType); + Tuple result = getTupleQuery( + spec, getDomainClass(), Sort.unsorted(), resultProcessor.getReturnedType()).getSingleResult(); return Optional.of(resultProcessor.processResult(result, new TupleConverter(projectionType))); } catch (NoResultException e) { return Optional.empty(); @@ -523,10 +510,11 @@ public Page findAll(@Nullable Specification spec, Pageable pageable) { public

Page

findAll(@Nullable Specification spec, Pageable pageable, Class

projectionType) { Sort sort = pageable.isPaged() ? pageable.getSort() : Sort.unsorted(); + ResultProcessor resultProcessor = getResultProcessor(projectionType); TypedQuery query = getTupleQuery( - spec, getDomainClass(), sort, projectionType); + spec, getDomainClass(), sort, resultProcessor.getReturnedType()); return isUnpaged(pageable) - ? new PageImpl

(getResultProcessor(projectionType).processResult(query.getResultList(), new TupleConverter(projectionType))) + ? new PageImpl

(resultProcessor.processResult(query.getResultList(), new TupleConverter(projectionType))) : readPage(query, getDomainClass(), pageable, spec, projectionType); } @@ -545,9 +533,9 @@ public List findAll(@Nullable Specification spec, Sort sort) { */ @Override public

List

findAll(@Nullable Specification spec, Sort sort, Class

projectionType) { - List resultList = getTupleQuery( - spec, getDomainClass(), sort, projectionType).getResultList(); ResultProcessor resultProcessor = getResultProcessor(projectionType); + List resultList = getTupleQuery( + spec, getDomainClass(), sort, resultProcessor.getReturnedType()).getResultList(); return resultProcessor.processResult(resultList, new TupleConverter(projectionType)); } @@ -828,36 +816,23 @@ protected TypedQuery getQuery(@Nullable Specification spec, * @param spec can be {@literal null}. * @param domainClass must not be {@literal null}. * @param sort must not be {@literal null}. - * @param projectionType must not be {@literal null}. + * @param returnedType must not be {@literal null}. * @return */ - protected TypedQuery getTupleQuery(@Nullable Specification spec, Class domainClass, Sort sort, Class projectionType) { + protected TypedQuery getTupleQuery(@Nullable Specification spec, Class domainClass, Sort sort, ReturnedType returnedType) { CriteriaBuilder builder = em.getCriteriaBuilder(); CriteriaQuery query = builder.createTupleQuery(); Root root = applySpecificationToCriteria(spec, domainClass, query); - - ProjectionInformation projectionInformation = projectionFactory.getProjectionInformation(projectionType); - List properties = null; - if (projectionType.isInterface()) { - properties = new ArrayList<>(); - for (PropertyDescriptor descriptor : projectionInformation.getInputProperties()) { - if (!properties.contains(descriptor.getName())) { - properties.add(descriptor.getName()); - } - } - } - else { - properties = detectConstructorParameterNames(projectionType); - } - List> selections = new ArrayList<>(); - for (String property : properties) { - PropertyPath path = PropertyPath.from(property, projectionType); - selections.add(toExpressionRecursively(root, path, true).alias(property)); - } - query.multiselect(selections); + List properties = returnedType.getInputProperties(); + List> selections = new ArrayList<>(properties.size()); + for (String property : properties) { + PropertyPath path = PropertyPath.from(property, returnedType.getReturnedType()); + selections.add(toExpressionRecursively(root, path, true).alias(property)); + } + query.multiselect(selections); if (sort.isSorted()) { query.orderBy(toOrders(sort, root, builder)); @@ -866,47 +841,6 @@ protected TypedQuery getTupleQuery(@Nullable Specificati return applyRepositoryMethodMetadata(em.createQuery(query)); } - private List detectConstructorParameterNames(Class type) { - - if (!isDto(type)) { - return Collections.emptyList(); - } - - PreferredConstructor constructor = PreferredConstructorDiscoverer.discover(type); - - if (constructor == null) { - return Collections.emptyList(); - } - - List properties = new ArrayList<>(constructor.getConstructor().getParameterCount()); - - for (PreferredConstructor.Parameter parameter : constructor.getParameters()) { - properties.add(parameter.getName()); - } - - return properties; - } - - private static final Set> VOID_TYPES = new HashSet<>(Arrays.asList(Void.class, void.class)); - - private boolean isDto(Class type) { - return !Object.class.equals(type) && // - !type.isEnum() && // - !isDomainSubtype(type) && // - !isPrimitiveOrWrapper(type) && // - !Number.class.isAssignableFrom(type) && // - !VOID_TYPES.contains(type) && // - !type.getPackage().getName().startsWith("java."); - } - - private boolean isDomainSubtype(Class type) { - return getDomainClass().equals(type) && getDomainClass().isAssignableFrom(type); - } - - private boolean isPrimitiveOrWrapper(Class type) { - return ClassUtils.isPrimitiveOrWrapper(type); - } - /** * Creates a new count query for the given {@link Specification}. *