Skip to content

DATAJPA-1033 Support projections on methods that take a Specification #430

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
*
* @author Oliver Gierke
* @author Christoph Strobl
* @author Lorenzo Dee
*/
public interface JpaSpecificationExecutor<T> {

Expand All @@ -41,6 +42,17 @@ public interface JpaSpecificationExecutor<T> {
*/
Optional<T> findOne(@Nullable Specification<T> 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.
*/
<P> Optional<P> findOne(@Nullable Specification<T> spec, Class<P> projectionType);

/**
* Returns all entities matching the given {@link Specification}.
*
Expand All @@ -49,6 +61,15 @@ public interface JpaSpecificationExecutor<T> {
*/
List<T> findAll(@Nullable Specification<T> 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}.
*/
<P> List<P> findAll(@Nullable Specification<T> spec, Class<P> projectionType);

/**
* Returns a {@link Page} of entities matching the given {@link Specification}.
*
Expand All @@ -58,6 +79,16 @@ public interface JpaSpecificationExecutor<T> {
*/
Page<T> findAll(@Nullable Specification<T> 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}.
*/
<P> Page<P> findAll(@Nullable Specification<T> spec, Pageable pageable, Class<P> projectionType);

/**
* Returns all entities matching the given {@link Specification} and {@link Sort}.
*
Expand All @@ -67,6 +98,16 @@ public interface JpaSpecificationExecutor<T> {
*/
List<T> findAll(@Nullable Specification<T> 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}.
*/
<P> List<P> findAll(@Nullable Specification<T> spec, Sort sort, Class<P> projectionType);

/**
* Returns the number of instances that the given {@link Specification} will return.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -59,6 +53,7 @@
* @author Nicolas Cirigliano
* @author Jens Schauder
* @author Сергей Цыпанов
* @author Lorenzo Dee
*/
public abstract class AbstractJpaQuery implements RepositoryQuery {

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -289,160 +284,4 @@ protected Class<?> getTypeToRead(ReturnedType returnedType) {
*/
protected abstract Query doCreateCountQuery(JpaParametersParameterAccessor accessor);

static class TupleConverter implements Converter<Object, Object> {

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<TupleElement<?>> 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<String, Object> {

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<? extends String, ?> m) {
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
}

@Override
public void clear() {
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
}

@Override
public Set<String> keySet() {

return tuple.getElements().stream() //
.map(TupleElement::getAlias) //
.collect(Collectors.toSet());
}

@Override
public Collection<Object> values() {
return Arrays.asList(tuple.toArray());
}

@Override
public Set<Entry<String, Object>> entrySet() {

return tuple.getElements().stream() //
.map(e -> new HashMap.SimpleEntry<String, Object>(e.getAlias(), tuple.get(e))) //
.collect(Collectors.toSet());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -615,12 +615,12 @@ private static javax.persistence.criteria.Order toJpaOrder(Order order, From<?,
}
}

static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property) {
public static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property) {
return toExpressionRecursively(from, property, false);
}

@SuppressWarnings("unchecked")
static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property, boolean isForSelection) {
public static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property, boolean isForSelection) {

Bindable<?> propertyPathModel;
Bindable<?> model = from.getModel();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}.
*
Expand Down Expand Up @@ -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}.
*
Expand Down Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@
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.
*
* @author Oliver Gierke
* @author Stefan Fussenegger
* @author Jens Schauder
* @author Lorenzo Dee
*/
@NoRepositoryBean
public interface JpaRepositoryImplementation<T, ID> extends JpaRepository<T, ID>, JpaSpecificationExecutor<T> {
Expand All @@ -45,4 +48,18 @@ public interface JpaRepositoryImplementation<T, ID> extends JpaRepository<T, ID>
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);
}
Loading