Skip to content

Commit 434d16b

Browse files
committedMay 16, 2019
use TypeQuery<Tuple> for selection projection
1 parent df08953 commit 434d16b

16 files changed

+978
-82
lines changed
 

Diff for: ‎pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@
7575
<artifactId>hibernate-jpamodelgen</artifactId>
7676
<scope>provided</scope>
7777
</dependency>
78+
<dependency>
79+
<groupId>org.projectlombok</groupId>
80+
<artifactId>lombok</artifactId>
81+
<version>1.18.4</version>
82+
<scope>provided</scope>
83+
</dependency>
7884
</dependencies>
7985

8086
<build>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package org.springframework.data.repository.query;
2+
3+
import lombok.NonNull;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.core.CollectionFactory;
6+
import org.springframework.core.convert.ConversionService;
7+
import org.springframework.core.convert.converter.Converter;
8+
import org.springframework.core.convert.support.DefaultConversionService;
9+
import org.springframework.data.domain.Slice;
10+
import org.springframework.data.projection.ProjectionFactory;
11+
import org.springframework.lang.Nullable;
12+
import org.springframework.util.Assert;
13+
14+
import java.util.*;
15+
16+
public class MyResultProcessor {
17+
private final ProjectingConverter converter;
18+
private final ProjectionFactory factory;
19+
private final ReturnedType type;
20+
21+
public MyResultProcessor(ProjectionFactory factory, ReturnedType type) {
22+
this.converter = new ProjectingConverter(type,factory).withType(type);
23+
this.factory = factory;
24+
this.type = type;
25+
}
26+
27+
public <T> T processResult(@Nullable Object source, Converter<Object, Object> preparingConverter) {
28+
29+
if (source == null || type.isInstance(source) || !type.isProjecting()) {
30+
return (T) source;
31+
}
32+
33+
Assert.notNull(preparingConverter, "Preparing converter must not be null!");
34+
35+
ChainingConverter converter = ChainingConverter.of(type.getReturnedType(), preparingConverter).and(this.converter);
36+
37+
if (source instanceof Slice ) {
38+
return (T) ((Slice<?>) source).map(converter::convert);
39+
}
40+
41+
if (source instanceof Collection ) {
42+
43+
Collection<?> collection = (Collection<?>) source;
44+
Collection<Object> target = createCollectionFor(collection);
45+
46+
for (Object columns : collection) {
47+
target.add(type.isInstance(columns) ? columns : converter.convert(columns));
48+
}
49+
50+
return (T) target;
51+
}
52+
return (T) converter.convert(source);
53+
}
54+
@RequiredArgsConstructor(staticName = "of")
55+
private static class ChainingConverter implements Converter<Object, Object> {
56+
57+
private final @NonNull
58+
Class<?> targetType;
59+
private final @NonNull Converter<Object, Object> delegate;
60+
61+
/**
62+
* Returns a new {@link ChainingConverter} that hands the elements resulting from the current conversion to the
63+
* given {@link Converter}.
64+
*
65+
* @param converter must not be {@literal null}.
66+
* @return
67+
*/
68+
public ChainingConverter and(final Converter<Object, Object> converter) {
69+
70+
Assert.notNull(converter, "Converter must not be null!");
71+
72+
return new ChainingConverter(targetType, source -> {
73+
74+
Object intermediate = ChainingConverter.this.convert(source);
75+
76+
return intermediate == null || targetType.isInstance(intermediate) ? intermediate
77+
: converter.convert(intermediate);
78+
});
79+
}
80+
81+
/*
82+
* (non-Javadoc)
83+
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
84+
*/
85+
@Nullable
86+
@Override
87+
public Object convert(Object source) {
88+
return delegate.convert(source);
89+
}
90+
}
91+
private static Collection<Object> createCollectionFor(Collection<?> source) {
92+
93+
try {
94+
return CollectionFactory.createCollection(source.getClass(), source.size());
95+
} catch (RuntimeException o_O) {
96+
return CollectionFactory.createApproximateCollection(source, source.size());
97+
}
98+
}
99+
100+
101+
@RequiredArgsConstructor
102+
private static class ProjectingConverter implements Converter<Object, Object> {
103+
104+
private final @NonNull ReturnedType type;
105+
private final @NonNull ProjectionFactory factory;
106+
private final @NonNull ConversionService conversionService;
107+
108+
/**
109+
* Creates a new {@link ProjectingConverter} for the given {@link ReturnedType} and {@link ProjectionFactory}.
110+
*
111+
* @param type must not be {@literal null}.
112+
* @param factory must not be {@literal null}.
113+
*/
114+
ProjectingConverter(ReturnedType type, ProjectionFactory factory) {
115+
this(type, factory, DefaultConversionService.getSharedInstance());
116+
}
117+
118+
/**
119+
* Creates a new {@link ProjectingConverter} for the given {@link ReturnedType}.
120+
*
121+
* @param type must not be {@literal null}.
122+
* @return
123+
*/
124+
ProjectingConverter withType(ReturnedType type) {
125+
126+
Assert.notNull(type, "ReturnedType must not be null!");
127+
128+
return new ProjectingConverter(type, factory, conversionService);
129+
}
130+
131+
/*
132+
* (non-Javadoc)
133+
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
134+
*/
135+
@Nullable
136+
@Override
137+
public Object convert(Object source) {
138+
139+
Class<?> targetType = type.getReturnedType();
140+
141+
if (targetType.isInterface()) {
142+
return factory.createProjection(targetType, getProjectionTarget(source));
143+
}
144+
145+
return conversionService.convert(source, targetType);
146+
}
147+
148+
private Object getProjectionTarget(Object source) {
149+
150+
if (source != null && source.getClass().isArray()) {
151+
source = Arrays.asList((Object[]) source);
152+
}
153+
154+
if (source instanceof Collection) {
155+
return toMap((Collection<?>) source, type.getInputProperties());
156+
}
157+
158+
return source;
159+
}
160+
161+
private static Map<String, Object> toMap(Collection<?> values, List<String> names) {
162+
163+
int i = 0;
164+
Map<String, Object> result = new HashMap<>(values.size());
165+
166+
for (Object element : values) {
167+
result.put(names.get(i++), element);
168+
}
169+
170+
return result;
171+
}
172+
}
173+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package org.springframework.data.repository.query;
2+
3+
import org.springframework.data.projection.ProjectionFactory;
4+
5+
public class ReturnTypeWarpper {
6+
public static ReturnedType of(Class<?> returnedType, Class<?> domainType, ProjectionFactory factory){
7+
return ReturnedType.of(returnedType, domainType,factory);
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
package org.springframework.data.repository.query;
2+
3+
import org.springframework.core.convert.converter.Converter;
4+
import org.springframework.lang.Nullable;
5+
import org.springframework.util.Assert;
6+
7+
import javax.persistence.Tuple;
8+
import javax.persistence.TupleElement;
9+
import java.util.*;
10+
import java.util.stream.Collectors;
11+
12+
public class TupleConverter implements Converter<Object, Object> {
13+
14+
private final ReturnedType type;
15+
16+
/**
17+
* Creates a new {@link TupleConverter} for the given {@link ReturnedType}.
18+
*
19+
* @param type must not be {@literal null}.
20+
*/
21+
public TupleConverter(ReturnedType type) {
22+
23+
Assert.notNull(type, "Returned type must not be null!");
24+
25+
this.type = type;
26+
}
27+
28+
/*
29+
* (non-Javadoc)
30+
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
31+
*/
32+
@Override
33+
public Object convert(Object source) {
34+
35+
if (!(source instanceof Tuple)) {
36+
return source;
37+
}
38+
39+
Tuple tuple = (Tuple) source;
40+
List<TupleElement<?>> elements = tuple.getElements();
41+
42+
if (elements.size() == 1) {
43+
44+
Object value = tuple.get(elements.get(0));
45+
46+
if (type.isInstance(value) || value == null) {
47+
return value;
48+
}
49+
}
50+
51+
return new TupleConverter.TupleBackedMap(tuple);
52+
}
53+
54+
/**
55+
* A {@link Map} implementation which delegates all calls to a {@link Tuple}. Depending on the provided
56+
* {@link Tuple} implementation it might return the same value for various keys of which only one will appear in the
57+
* key/entry set.
58+
*
59+
* @author Jens Schauder
60+
*/
61+
private static class TupleBackedMap implements Map<String, Object> {
62+
63+
private static final String UNMODIFIABLE_MESSAGE = "A TupleBackedMap cannot be modified.";
64+
65+
private final Tuple tuple;
66+
67+
TupleBackedMap(Tuple tuple) {
68+
this.tuple = tuple;
69+
}
70+
71+
@Override
72+
public int size() {
73+
return tuple.getElements().size();
74+
}
75+
76+
@Override
77+
public boolean isEmpty() {
78+
return tuple.getElements().isEmpty();
79+
}
80+
81+
/**
82+
* If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code false}.
83+
* Otherwise this returns {@code true} even when the value from the backing {@code Tuple} is {@code null}.
84+
*
85+
* @param key the key for which to get the value from the map.
86+
* @return wether the key is an element of the backing tuple.
87+
*/
88+
@Override
89+
public boolean containsKey(Object key) {
90+
91+
try {
92+
tuple.get((String) key);
93+
return true;
94+
} catch (IllegalArgumentException e) {
95+
return false;
96+
}
97+
}
98+
99+
@Override
100+
public boolean containsValue(Object value) {
101+
return Arrays.asList(tuple.toArray()).contains(value);
102+
}
103+
104+
/**
105+
* If the key is not a {@code String} or not a key of the backing {@link Tuple} this returns {@code null}.
106+
* Otherwise the value from the backing {@code Tuple} is returned, which also might be {@code null}.
107+
*
108+
* @param key the key for which to get the value from the map.
109+
* @return the value of the backing {@link Tuple} for that key or {@code null}.
110+
*/
111+
@Override
112+
@Nullable
113+
public Object get(Object key) {
114+
115+
if (!(key instanceof String)) {
116+
return null;
117+
}
118+
119+
try {
120+
return tuple.get((String) key);
121+
} catch (IllegalArgumentException e) {
122+
return null;
123+
}
124+
}
125+
126+
@Override
127+
public Object put(String key, Object value) {
128+
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
129+
}
130+
131+
@Override
132+
public Object remove(Object key) {
133+
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
134+
}
135+
136+
@Override
137+
public void putAll(Map<? extends String, ?> m) {
138+
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
139+
}
140+
141+
@Override
142+
public void clear() {
143+
throw new UnsupportedOperationException(UNMODIFIABLE_MESSAGE);
144+
}
145+
146+
@Override
147+
public Set<String> keySet() {
148+
149+
return tuple.getElements().stream() //
150+
.map(TupleElement::getAlias) //
151+
.collect(Collectors.toSet());
152+
}
153+
154+
@Override
155+
public Collection<Object> values() {
156+
return Arrays.asList(tuple.toArray());
157+
}
158+
159+
@Override
160+
public Set<Entry<String, Object>> entrySet() {
161+
162+
return tuple.getElements().stream() //
163+
.map(e -> new HashMap.SimpleEntry<String, Object>(e.getAlias(), tuple.get(e))) //
164+
.collect(Collectors.toSet());
165+
}
166+
}
167+
}
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
package th.co.geniustree.springdata.jpa.repository;
22

3-
import java.util.List;
4-
import java.util.Optional;
5-
63
import org.springframework.data.domain.Page;
74
import org.springframework.data.domain.Pageable;
85
import org.springframework.data.jpa.domain.Specification;
96
import org.springframework.data.jpa.repository.EntityGraph;
107
import org.springframework.data.jpa.repository.query.JpaEntityGraph;
118
import org.springframework.data.repository.NoRepositoryBean;
129

10+
import java.util.Optional;
11+
1312
/**
1413
* Created by pramoth on 9/29/2016 AD.
1514
*/
@@ -20,7 +19,28 @@ public interface JpaSpecificationExecutorWithProjection<T> {
2019

2120
<R> Page<R> findAll(Specification<T> spec, Class<R> projectionClass, Pageable pageable);
2221

22+
/**
23+
* Use Spring Data Annotation instead of manually provide EntityGraph.
24+
* @param spec
25+
* @param projectionType
26+
* @param namedEntityGraph
27+
* @param type
28+
* @param pageable
29+
* @param <R>
30+
* @return
31+
*/
32+
@Deprecated
2333
<R> Page<R> findAll(Specification<T> spec, Class<R> projectionType, String namedEntityGraph, EntityGraph.EntityGraphType type, Pageable pageable);
2434

35+
/**
36+
* Use Spring Data Annotation instead of manually provide EntityGraph.
37+
* @param spec
38+
* @param projectionClass
39+
* @param dynamicEntityGraph
40+
* @param pageable
41+
* @param <R>
42+
* @return
43+
*/
44+
@Deprecated
2545
<R> Page<R> findAll(Specification<T> spec, Class<R> projectionClass, JpaEntityGraph dynamicEntityGraph, Pageable pageable);
2646
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright 2017-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package th.co.geniustree.springdata.jpa.repository.support;
17+
18+
import org.springframework.data.jpa.repository.EntityGraph;
19+
import org.springframework.data.jpa.repository.query.Jpa21Utils;
20+
import org.springframework.data.jpa.repository.query.JpaEntityGraph;
21+
import org.springframework.data.jpa.repository.support.CrudMethodMetadata;
22+
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
23+
import org.springframework.data.util.Optionals;
24+
import org.springframework.util.Assert;
25+
26+
import javax.persistence.EntityManager;
27+
import java.util.*;
28+
import java.util.Map.Entry;
29+
30+
/**
31+
* Default implementation of {@link QueryHints}.
32+
*
33+
* @author Christoph Strobl
34+
* @author Oliver Gierke
35+
* @author Jens Schauder
36+
* @since 2.0
37+
*/
38+
class DefaultQueryHints implements QueryHints {
39+
40+
private final JpaEntityInformation<?, ?> information;
41+
private final CrudMethodMetadata metadata;
42+
private final Optional<EntityManager> entityManager;
43+
private final boolean forCounts;
44+
45+
/**
46+
* Creates a new {@link DefaultQueryHints} instance for the given {@link JpaEntityInformation},
47+
* {@link CrudMethodMetadata}, {@link EntityManager} and whether to include fetch graphs.
48+
*
49+
* @param information must not be {@literal null}.
50+
* @param metadata must not be {@literal null}.
51+
* @param entityManager must not be {@literal null}.
52+
* @param forCounts
53+
*/
54+
private DefaultQueryHints(JpaEntityInformation<?, ?> information, CrudMethodMetadata metadata,
55+
Optional<EntityManager> entityManager, boolean forCounts) {
56+
57+
this.information = information;
58+
this.metadata = metadata;
59+
this.entityManager = entityManager;
60+
this.forCounts = forCounts;
61+
}
62+
63+
/**
64+
* Creates a new {@link QueryHints} instance for the given {@link JpaEntityInformation}, {@link CrudMethodMetadata}
65+
* and {@link EntityManager}.
66+
*
67+
* @param information must not be {@literal null}.
68+
* @param metadata must not be {@literal null}.
69+
* @return
70+
*/
71+
public static QueryHints of(JpaEntityInformation<?, ?> information, CrudMethodMetadata metadata) {
72+
73+
Assert.notNull(information, "JpaEntityInformation must not be null!");
74+
Assert.notNull(metadata, "CrudMethodMetadata must not be null!");
75+
76+
return new DefaultQueryHints(information, metadata, Optional.empty(), false);
77+
}
78+
79+
/*
80+
* (non-Javadoc)
81+
* @see org.springframework.data.jpa.repository.support.QueryHints#withFetchGraphs()
82+
*/
83+
@Override
84+
public QueryHints withFetchGraphs(EntityManager em) {
85+
return new DefaultQueryHints(this.information, this.metadata, Optional.of(em), this.forCounts);
86+
}
87+
88+
/*
89+
* (non-Javadoc)
90+
* @see org.springframework.data.jpa.repository.support.QueryHints#forCounts()
91+
*/
92+
@Override
93+
public QueryHints forCounts() {
94+
return new DefaultQueryHints(this.information, this.metadata, this.entityManager, true);
95+
}
96+
97+
/*
98+
* (non-Javadoc)
99+
* @see java.lang.Iterable#iterator()
100+
*/
101+
@Override
102+
public Iterator<Entry<String, Object>> iterator() {
103+
return asMap().entrySet().iterator();
104+
}
105+
106+
/*
107+
* (non-Javadoc)
108+
* @see org.springframework.data.jpa.repository.support.QueryHints#asMap()
109+
*/
110+
@Override
111+
public Map<String, Object> asMap() {
112+
113+
Map<String, Object> hints = new HashMap<>();
114+
115+
if (forCounts) {
116+
//hints.putAll(metadata.getQueryHintsForCount());
117+
} else {
118+
hints.putAll(metadata.getQueryHints());
119+
}
120+
121+
hints.putAll(getFetchGraphs());
122+
123+
return hints;
124+
}
125+
126+
private Map<String, Object> getFetchGraphs() {
127+
128+
return Optionals
129+
.mapIfAllPresent(entityManager, metadata.getEntityGraph(),
130+
(em, graph) -> Jpa21Utils.tryGetFetchGraphHints(em, getEntityGraph(graph), information.getJavaType()))
131+
.orElse(Collections.emptyMap());
132+
}
133+
134+
private JpaEntityGraph getEntityGraph(EntityGraph entityGraph) {
135+
136+
String fallbackName = information.getEntityName() + "." + metadata.getMethod().getName();
137+
return new JpaEntityGraph(entityGraph, fallbackName);
138+
}
139+
}

Diff for: ‎src/main/java/th/co/geniustree/springdata/jpa/repository/support/JpaSpecificationExecutorWithProjectionImpl.java

+231-28
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,60 @@
22

33
import org.slf4j.Logger;
44
import org.slf4j.LoggerFactory;
5+
import org.springframework.core.annotation.AnnotationUtils;
56
import org.springframework.data.domain.Page;
67
import org.springframework.data.domain.PageImpl;
78
import org.springframework.data.domain.Pageable;
89
import org.springframework.data.domain.Sort;
910
import org.springframework.data.jpa.domain.Specification;
10-
import org.springframework.data.jpa.repository.query.Jpa21Utils;
1111
import org.springframework.data.jpa.repository.query.JpaEntityGraph;
12+
import org.springframework.data.jpa.repository.query.QueryUtils;
1213
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
1314
import org.springframework.data.jpa.repository.support.SimpleJpaRepository;
15+
import org.springframework.data.mapping.PropertyPath;
1416
import org.springframework.data.projection.ProjectionFactory;
1517
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
18+
import org.springframework.data.repository.query.MyResultProcessor;
19+
import org.springframework.data.repository.query.ReturnTypeWarpper;
20+
import org.springframework.data.repository.query.ReturnedType;
21+
import org.springframework.data.repository.query.TupleConverter;
22+
import org.springframework.data.repository.support.PageableExecutionUtils;
23+
import org.springframework.lang.Nullable;
24+
import org.springframework.util.Assert;
1625
import th.co.geniustree.springdata.jpa.repository.JpaSpecificationExecutorWithProjection;
1726

1827
import javax.persistence.*;
28+
import javax.persistence.criteria.*;
29+
import javax.persistence.metamodel.Attribute;
30+
import javax.persistence.metamodel.Bindable;
31+
import javax.persistence.metamodel.ManagedType;
32+
import javax.persistence.metamodel.PluralAttribute;
1933
import java.io.Serializable;
20-
import java.util.HashMap;
21-
import java.util.Map;
22-
import java.util.Optional;
34+
import java.lang.annotation.Annotation;
35+
import java.lang.reflect.AnnotatedElement;
36+
import java.lang.reflect.Member;
37+
import java.util.*;
38+
39+
import static javax.persistence.metamodel.Attribute.PersistentAttributeType.*;
40+
2341

2442
/**
2543
* Created by pramoth on 9/29/2016 AD.
2644
*/
2745
public class JpaSpecificationExecutorWithProjectionImpl<T, ID extends Serializable> extends SimpleJpaRepository<T, ID> implements JpaSpecificationExecutorWithProjection<T> {
2846

2947
private static final Logger log = LoggerFactory.getLogger(JpaSpecificationExecutorWithProjectionImpl.class);
48+
private static final Map<Attribute.PersistentAttributeType, Class<? extends Annotation>> ASSOCIATION_TYPES;
49+
static{
50+
Map<Attribute.PersistentAttributeType, Class<? extends Annotation>> persistentAttributeTypes = new HashMap<Attribute.PersistentAttributeType, Class<? extends Annotation>>();
51+
persistentAttributeTypes.put(ONE_TO_ONE, OneToOne.class);
52+
persistentAttributeTypes.put(ONE_TO_MANY, null);
53+
persistentAttributeTypes.put(MANY_TO_ONE, ManyToOne.class);
54+
persistentAttributeTypes.put(MANY_TO_MANY, null);
55+
persistentAttributeTypes.put(ELEMENT_COLLECTION, null);
3056

57+
ASSOCIATION_TYPES = Collections.unmodifiableMap(persistentAttributeTypes);
58+
}
3159
private final EntityManager entityManager;
3260

3361
private final ProjectionFactory projectionFactory = new SpelAwareProxyProjectionFactory();
@@ -43,52 +71,227 @@ public JpaSpecificationExecutorWithProjectionImpl(JpaEntityInformation entityInf
4371

4472
@Override
4573
public <R> Optional<R> findOne(Specification<T> spec, Class<R> projectionType) {
46-
TypedQuery<T> query = getQuery(spec, Sort.unsorted());
74+
final ReturnedType returnedType = ReturnTypeWarpper.of(projectionType, getDomainClass(), projectionFactory);
75+
final TypedQuery<Tuple> query = getTupleQuery(spec, Sort.unsorted(), returnedType);
4776
try {
48-
T result = query.getSingleResult();
49-
return Optional.ofNullable(projectionFactory.createProjection(projectionType, result));
77+
Tuple result = query.getSingleResult();
78+
final MyResultProcessor resultProcessor = new MyResultProcessor(projectionFactory,returnedType);
79+
final R singleResult = resultProcessor.processResult(result, new TupleConverter(returnedType));
80+
return Optional.ofNullable(singleResult);
5081
} catch (NoResultException e) {
5182
return Optional.empty();
5283
}
5384
}
5485

5586
@Override
5687
public <R> Page<R> findAll(Specification<T> spec, Class<R> projectionType, Pageable pageable) {
57-
TypedQuery<T> query = getQuery(spec, pageable);
58-
return readPageWithProjection(spec, projectionType, pageable, query);
88+
final ReturnedType returnedType = ReturnTypeWarpper.of(projectionType, getDomainClass(), projectionFactory);
89+
final TypedQuery<Tuple> query = getTupleQuery(spec, pageable.isPaged() ? pageable.getSort() : Sort.unsorted(), returnedType);
90+
final MyResultProcessor resultProcessor = new MyResultProcessor(projectionFactory,returnedType);
91+
if (pageable.isPaged()) {
92+
query.setFirstResult((int)pageable.getOffset());
93+
query.setMaxResults(pageable.getPageSize());
94+
}
95+
final List<R> resultList = resultProcessor.processResult(query.getResultList(), new TupleConverter(returnedType));
96+
final Page<R> page = PageableExecutionUtils.getPage(resultList, pageable, () -> executeCountQuery(this.getCountQuery(spec, getDomainClass())));
97+
return pageable.isUnpaged() ? new PageImpl(resultList) : page;
98+
}
99+
100+
static Long executeCountQuery(TypedQuery<Long> query) {
101+
Assert.notNull(query, "TypedQuery must not be null!");
102+
List<Long> totals = query.getResultList();
103+
Long total = 0L;
104+
105+
Long element;
106+
for(Iterator var3 = totals.iterator(); var3.hasNext(); total = total + (element == null ? 0L : element)) {
107+
element = (Long)var3.next();
108+
}
109+
110+
return total;
59111
}
60112

61113
@Override
62114
public <R> Page<R> findAll(Specification<T> spec, Class<R> projectionType, String namedEntityGraph, org.springframework.data.jpa.repository.EntityGraph.EntityGraphType type, Pageable pageable) {
63-
EntityGraph<?> entityGraph = this.entityManager.getEntityGraph(namedEntityGraph);
64-
if (entityGraph == null) {
65-
throw new IllegalArgumentException("Not found named entity graph -> " + namedEntityGraph);
66-
}
67-
TypedQuery<T> query = getQuery(spec, pageable);
68-
query.setHint(type.getKey(), entityGraph);
69-
return readPageWithProjection(spec, projectionType, pageable, query);
115+
return findAll(spec,projectionType,pageable);
70116
}
71117

72118
@Override
73119
public <R> Page<R> findAll(Specification<T> spec, Class<R> projectionType, JpaEntityGraph dynamicEntityGraph, Pageable pageable) {
74-
TypedQuery<T> query = getQuery(spec, pageable);
75-
Map<String, Object> entityGraphHints = new HashMap<String, Object>();
76-
entityGraphHints.putAll(Jpa21Utils.tryGetFetchGraphHints(this.entityManager, dynamicEntityGraph, getDomainClass()));
77-
applyEntityGraphQueryHints(query, entityGraphHints);
78-
return readPageWithProjection(spec, projectionType, pageable, query);
120+
return findAll(spec,projectionType,pageable);
121+
}
122+
123+
protected TypedQuery<Tuple> getTupleQuery(@Nullable Specification spec, Sort sort, ReturnedType returnedType) {
124+
CriteriaBuilder builder = this.entityManager.getCriteriaBuilder();
125+
CriteriaQuery<Tuple> query = builder.createQuery(Tuple.class);
126+
Root<T> root = this.applySpecificationToCriteria(spec, getDomainClass(), query);
127+
Predicate predicate = spec.toPredicate(root, query, builder);
128+
129+
if (predicate != null) {
130+
query.where(predicate);
131+
}
132+
if (returnedType.isProjecting()) {
133+
List<Selection<?>> selections = new ArrayList<>();
134+
135+
for (String property : returnedType.getInputProperties()) {
136+
PropertyPath path = PropertyPath.from(property, returnedType.getReturnedType());
137+
selections.add(toExpressionRecursively(root, path, true).alias(property));
138+
}
139+
140+
query.multiselect(selections);
141+
} else {
142+
throw new IllegalArgumentException("only except projection");
143+
}
144+
if (sort.isSorted()) {
145+
query.orderBy(QueryUtils.toOrders(sort, root, builder));
146+
}
147+
148+
return this.applyRepositoryMethodMetadata(this.entityManager.createQuery(query));
79149
}
80150

81-
private <R> Page<R> readPageWithProjection(Specification<T> spec, Class<R> projectionType, Pageable pageable, TypedQuery<T> query) {
82-
if (log.isDebugEnabled()) {
83-
query.getHints().forEach((key, value) -> log.info("apply query hints -> {} : {}", key, value));
151+
152+
private <S, U extends T> Root<U> applySpecificationToCriteria(@Nullable Specification<U> spec, Class<U> domainClass,
153+
CriteriaQuery<S> query) {
154+
155+
Assert.notNull(domainClass, "Domain class must not be null!");
156+
Assert.notNull(query, "CriteriaQuery must not be null!");
157+
158+
Root<U> root = query.from(domainClass);
159+
160+
if (spec == null) {
161+
return root;
162+
}
163+
164+
CriteriaBuilder builder = entityManager.getCriteriaBuilder();
165+
Predicate predicate = spec.toPredicate(root, query, builder);
166+
167+
if (predicate != null) {
168+
query.where(predicate);
84169
}
85-
Page<T> result = pageable.isUnpaged() ? new PageImpl<>(query.getResultList()) : readPage(query, getDomainClass(), pageable, spec);
86-
return result.map(item -> projectionFactory.createProjection(projectionType, item));
170+
171+
return root;
87172
}
88173

89-
private void applyEntityGraphQueryHints(Query query, Map<String, Object> hints) {
90-
for (Map.Entry<String, Object> hint : hints.entrySet()) {
174+
private <S> TypedQuery<S> applyRepositoryMethodMetadata(TypedQuery<S> query) {
175+
176+
if (getRepositoryMethodMetadata() == null) {
177+
return query;
178+
}
179+
180+
LockModeType type = getRepositoryMethodMetadata().getLockModeType();
181+
TypedQuery<S> toReturn = type == null ? query : query.setLockMode(type);
182+
applyQueryHints(toReturn);
183+
184+
return toReturn;
185+
}
186+
private void applyQueryHints(Query query) {
187+
QueryHints queryHints = DefaultQueryHints.of(this.entityInformation, getRepositoryMethodMetadata());
188+
if(queryHints==null){
189+
queryHints = QueryHints.NoHints.INSTANCE;
190+
}
191+
for (Map.Entry<String, Object> hint : queryHints.withFetchGraphs(this.entityManager)) {
91192
query.setHint(hint.getKey(), hint.getValue());
92193
}
93194
}
195+
196+
197+
static Expression<Object> toExpressionRecursively(Path<Object> path, PropertyPath property) {
198+
199+
Path<Object> result = path.get(property.getSegment());
200+
return property.hasNext() ? toExpressionRecursively(result, property.next()) : result;
201+
}
202+
203+
static <T> Expression<T> toExpressionRecursively(From<?, ?> from, PropertyPath property, boolean isForSelection) {
204+
205+
Bindable<?> propertyPathModel;
206+
Bindable<?> model = from.getModel();
207+
String segment = property.getSegment();
208+
209+
if (model instanceof ManagedType) {
210+
211+
/*
212+
* Required to keep support for EclipseLink 2.4.x. TODO: Remove once we drop that (probably Dijkstra M1)
213+
* See: https://bugs.eclipse.org/bugs/show_bug.cgi?id=413892
214+
*/
215+
propertyPathModel = (Bindable<?>) ((ManagedType<?>) model).getAttribute(segment);
216+
} else {
217+
propertyPathModel = from.get(segment).getModel();
218+
}
219+
220+
if (requiresJoin(propertyPathModel, model instanceof PluralAttribute, !property.hasNext(), isForSelection)
221+
&& !isAlreadyFetched(from, segment)) {
222+
Join<?, ?> join = getOrCreateJoin(from, segment);
223+
return (Expression<T>) (property.hasNext() ? toExpressionRecursively(join, property.next(), isForSelection)
224+
: join);
225+
} else {
226+
Path<Object> path = from.get(segment);
227+
return (Expression<T>) (property.hasNext() ? toExpressionRecursively(path, property.next()) : path);
228+
}
229+
}
230+
231+
private static boolean requiresJoin(@Nullable Bindable<?> propertyPathModel, boolean isPluralAttribute,
232+
boolean isLeafProperty, boolean isForSelection) {
233+
234+
if (propertyPathModel == null && isPluralAttribute) {
235+
return true;
236+
}
237+
238+
if (!(propertyPathModel instanceof Attribute)) {
239+
return false;
240+
}
241+
242+
Attribute<?, ?> attribute = (Attribute<?, ?>) propertyPathModel;
243+
244+
if (!ASSOCIATION_TYPES.containsKey(attribute.getPersistentAttributeType())) {
245+
return false;
246+
}
247+
248+
// if this path is part of the select list we need to generate an explicit outer join in order to prevent Hibernate
249+
// to use an inner join instead.
250+
// see https://hibernate.atlassian.net/browse/HHH-12999.
251+
if (isLeafProperty && !isForSelection && !attribute.isCollection()) {
252+
return false;
253+
}
254+
255+
Class<? extends Annotation> associationAnnotation = ASSOCIATION_TYPES.get(attribute.getPersistentAttributeType());
256+
257+
if (associationAnnotation == null) {
258+
return true;
259+
}
260+
261+
Member member = attribute.getJavaMember();
262+
263+
if (!(member instanceof AnnotatedElement)) {
264+
return true;
265+
}
266+
267+
Annotation annotation = AnnotationUtils.getAnnotation((AnnotatedElement) member, associationAnnotation);
268+
return annotation == null ? true : (boolean) AnnotationUtils.getValue(annotation, "optional");
269+
}
270+
271+
private static Join<?, ?> getOrCreateJoin(From<?, ?> from, String attribute) {
272+
273+
for (Join<?, ?> join : from.getJoins()) {
274+
275+
boolean sameName = join.getAttribute().getName().equals(attribute);
276+
277+
if (sameName && join.getJoinType().equals(JoinType.LEFT)) {
278+
return join;
279+
}
280+
}
281+
282+
return from.join(attribute, JoinType.LEFT);
283+
}
284+
private static boolean isAlreadyFetched(From<?, ?> from, String attribute) {
285+
286+
for (Fetch<?, ?> fetch : from.getFetches()) {
287+
288+
boolean sameName = fetch.getAttribute().getName().equals(attribute);
289+
290+
if (sameName && fetch.getJoinType().equals(JoinType.LEFT)) {
291+
return true;
292+
}
293+
}
294+
295+
return false;
296+
}
94297
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
* Copyright 2017-2019 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package th.co.geniustree.springdata.jpa.repository.support;
17+
18+
import org.springframework.data.jpa.repository.support.CrudMethodMetadata;
19+
20+
import javax.persistence.EntityManager;
21+
import java.util.Collections;
22+
import java.util.Iterator;
23+
import java.util.Map;
24+
import java.util.Map.Entry;
25+
26+
/**
27+
* QueryHints provides access to query hints defined via {@link CrudMethodMetadata#getQueryHints()} by default excluding
28+
* JPA {@link javax.persistence.EntityGraph}.
29+
*
30+
* @author Christoph Strobl
31+
* @author Oliver Gierke
32+
* @author Jens Schauder
33+
* @since 2.0
34+
*/
35+
interface QueryHints extends Iterable<Entry<String, Object>> {
36+
37+
/**
38+
* Creates and returns a new {@link QueryHints} instance including {@link javax.persistence.EntityGraph}.
39+
*
40+
* @param em must not be {@literal null}.
41+
* @return new instance of {@link QueryHints}.
42+
*/
43+
QueryHints withFetchGraphs(EntityManager em);
44+
45+
/**
46+
* Creates and returns a new {@link QueryHints} instance that will contain only those hints applicable for count
47+
* queries.
48+
*
49+
* @return new instance of {@link QueryHints}.
50+
* @since 2.2
51+
*/
52+
QueryHints forCounts();
53+
54+
/**
55+
* Get the query hints as a {@link Map}.
56+
*
57+
* @return never {@literal null}.
58+
*/
59+
Map<String, Object> asMap();
60+
61+
/**
62+
* Null object implementation of {@link QueryHints}.
63+
*
64+
* @author Oliver Gierke
65+
* @since 2.0
66+
*/
67+
static enum NoHints implements QueryHints {
68+
69+
INSTANCE;
70+
71+
/*
72+
* (non-Javadoc)
73+
* @see org.springframework.data.jpa.repository.support.QueryHints#asMap()
74+
*/
75+
@Override
76+
public Map<String, Object> asMap() {
77+
return Collections.emptyMap();
78+
}
79+
80+
/*
81+
* (non-Javadoc)
82+
* @see java.lang.Iterable#iterator()
83+
*/
84+
@Override
85+
public Iterator<Entry<String, Object>> iterator() {
86+
return Collections.emptyIterator();
87+
}
88+
89+
/*
90+
* (non-Javadoc)
91+
* @see org.springframework.data.jpa.repository.support.QueryHints#withFetchGraphs(javax.persistence.EntityManager)
92+
*/
93+
@Override
94+
public QueryHints withFetchGraphs(EntityManager em) {
95+
return this;
96+
}
97+
98+
/*
99+
* (non-Javadoc)
100+
* @see org.springframework.data.jpa.repository.support.QueryHints#forCounts(javax.persistence.EntityManager)
101+
*/
102+
@Override
103+
public QueryHints forCounts() {
104+
return this;
105+
}
106+
}
107+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package org.springframework.data.projection;
2+
3+
import org.assertj.core.api.Assertions;
4+
import org.junit.Test;
5+
import th.co.geniustree.springdata.jpa.repository.DocumentRepository;
6+
7+
public class PorpertiesSourceTest {
8+
@Test
9+
public void test() {
10+
DefaultProjectionInformation projectionInformation = new DefaultProjectionInformation(DocumentRepository.DocumentWithoutParent.class);
11+
projectionInformation.getInputProperties().forEach(e -> {
12+
System.out.println(e.getName());
13+
});
14+
Assertions.assertThat(projectionInformation.isClosed()).isTrue();
15+
}
16+
}

Diff for: ‎src/test/java/th/co/geniustree/springdata/jpa/DemoApplication.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
package th.co.geniustree.springdata.jpa;
22

3-
import th.co.geniustree.springdata.jpa.repository.support.JpaSpecificationExecutorWithProjectionImpl;
43
import org.springframework.boot.SpringApplication;
54
import org.springframework.boot.autoconfigure.SpringBootApplication;
65
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
6+
import th.co.geniustree.springdata.jpa.repository.support.JpaSpecificationExecutorWithProjectionImpl;
77

88
@SpringBootApplication
99
@EnableJpaRepositories(repositoryBaseClass = JpaSpecificationExecutorWithProjectionImpl.class)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package th.co.geniustree.springdata.jpa;
2+
3+
import org.assertj.core.api.Assertions;
4+
import org.junit.Test;
5+
import org.junit.runner.RunWith;
6+
import org.springframework.beans.factory.annotation.Autowired;
7+
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
8+
import org.springframework.boot.test.context.SpringBootTest;
9+
import org.springframework.data.domain.Page;
10+
import org.springframework.data.domain.PageRequest;
11+
import org.springframework.data.jpa.domain.Specification;
12+
import org.springframework.test.context.junit4.SpringRunner;
13+
import org.springframework.transaction.annotation.Transactional;
14+
import th.co.geniustree.springdata.jpa.domain.Document;
15+
import th.co.geniustree.springdata.jpa.repository.DocumentRepository;
16+
import th.co.geniustree.springdata.jpa.specification.DocumentSpecs;
17+
18+
import java.util.Optional;
19+
20+
@RunWith(SpringRunner.class)
21+
@SpringBootTest
22+
@DataJpaTest
23+
@Transactional
24+
public class SpecificationExecutorProjectionTest {
25+
@Autowired
26+
private DocumentRepository documentRepository;
27+
28+
29+
@Test
30+
public void findAll() {
31+
Specification<Document> where = Specification.where(DocumentSpecs.idEq(1L));
32+
Page<DocumentRepository.DocumentWithoutParent> all = documentRepository.findAll(where, DocumentRepository.DocumentWithoutParent.class, PageRequest.of(0,10));
33+
Assertions.assertThat(all).isNotEmpty();
34+
Assertions.assertThat(all.getContent().get(0).getDocumentType()).isEqualTo("ต้นฉบับ");
35+
}
36+
37+
@Test
38+
public void findAll2() {
39+
Specification<Document> where = Specification.where(DocumentSpecs.idEq(1L));
40+
Page<DocumentRepository.DocumentWithoutParent> all = documentRepository.findAll(where, DocumentRepository.DocumentWithoutParent.class, PageRequest.of(0,10));
41+
Assertions.assertThat(all).isNotEmpty();
42+
Assertions.assertThat(all.getContent().get(0).getChild().size()).isEqualTo(1);
43+
}
44+
45+
@Test
46+
public void findAll3() {
47+
Specification<Document> where = Specification.where(DocumentSpecs.idEq(1L));
48+
Page<DocumentRepository.OnlyId> all = documentRepository.findAll(where, DocumentRepository.OnlyId.class, PageRequest.of(0,10));
49+
Assertions.assertThat(all).isNotEmpty();
50+
Assertions.assertThat(all.getContent().get(0).getId()).isEqualTo(1L);
51+
}
52+
53+
@Test
54+
public void findAll4() {
55+
Specification<Document> where = Specification.where(DocumentSpecs.idEq(24L));
56+
Page<DocumentRepository.DocumentWithoutParent> all = documentRepository.findAll(where, DocumentRepository.DocumentWithoutParent.class, PageRequest.of(0,10));
57+
Assertions.assertThat(all).isNotEmpty();
58+
Assertions.assertThat(all.getContent().get(0).getChild()).isNull();
59+
}
60+
61+
@Test
62+
public void findAll5() {
63+
Specification<Document> where = Specification.where(DocumentSpecs.idEq(24L));
64+
Page<DocumentRepository.OnlyParent> all = documentRepository.findAll(where, DocumentRepository.OnlyParent.class, PageRequest.of(0,10));
65+
Assertions.assertThat(all).isNotEmpty();
66+
Assertions.assertThat(all.getContent().get(0).getParent().getId()).isEqualTo(13L);
67+
}
68+
@Test
69+
public void find_single_page() {
70+
Specification<Document> where = Specification.where(DocumentSpecs.idEq(24L));
71+
Page<DocumentRepository.OnlyParent> all = documentRepository.findAll(where, DocumentRepository.OnlyParent.class, PageRequest.of(0,10));
72+
Assertions.assertThat(all).isNotEmpty();
73+
Assertions.assertThat(all.getTotalElements()).isEqualTo(1);
74+
Assertions.assertThat(all.getTotalPages()).isEqualTo(1);
75+
}
76+
77+
@Test
78+
public void find_all_page() {
79+
Specification<Document> where = Specification.where(null);
80+
Page<DocumentRepository.OnlyParent> all = documentRepository.findAll(where, DocumentRepository.OnlyParent.class, PageRequest.of(0,10));
81+
Assertions.assertThat(all).isNotEmpty();
82+
Assertions.assertThat(all.getTotalElements()).isEqualTo(24);
83+
Assertions.assertThat(all.getTotalPages()).isEqualTo(3);
84+
}
85+
86+
@Test
87+
public void findOne() {
88+
Specification<Document> where = Specification.where(DocumentSpecs.idEq(1L));
89+
Optional<DocumentRepository.DocumentWithoutParent> one = documentRepository.findOne(where, DocumentRepository.DocumentWithoutParent.class);
90+
Assertions.assertThat(one.get().getDocumentType()).isEqualTo("ต้นฉบับ");
91+
}
92+
93+
94+
}

Diff for: ‎src/test/java/th/co/geniustree/springdata/jpa/SpecificationExecutorProjectionTests.java

-46
This file was deleted.

Diff for: ‎src/test/java/th/co/geniustree/springdata/jpa/repository/DocumentRepository.java

+8-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package th.co.geniustree.springdata.jpa.repository;
22

3-
import th.co.geniustree.springdata.jpa.domain.Document;
43
import org.springframework.data.jpa.repository.JpaRepository;
4+
import th.co.geniustree.springdata.jpa.domain.Document;
55

66
import java.util.List;
77

@@ -18,4 +18,11 @@ public static interface DocumentWithoutParent{
1818
String getDocumentCategory();
1919
List<DocumentWithoutParent> getChild();
2020
}
21+
public static interface OnlyId{
22+
Long getId();
23+
}
24+
25+
public static interface OnlyParent extends OnlyId{
26+
OnlyId getParent();
27+
}
2128
}

Diff for: ‎src/test/java/th/co/geniustree/springdata/jpa/repository/DocumentRepository2.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package th.co.geniustree.springdata.jpa.repository;
22

3-
import th.co.geniustree.springdata.jpa.domain.Document;
43
import org.springframework.data.jpa.repository.JpaRepository;
4+
import th.co.geniustree.springdata.jpa.domain.Document;
55

66
import java.util.List;
77

Diff for: ‎src/test/java/th/co/geniustree/springdata/jpa/specification/DocumentSpecs.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package th.co.geniustree.springdata.jpa.specification;
22

3+
import org.springframework.data.jpa.domain.Specification;
34
import th.co.geniustree.springdata.jpa.domain.Document;
45
import th.co.geniustree.springdata.jpa.domain.Document_;
5-
import org.springframework.data.jpa.domain.Specification;
66

77
/**
88
* Created by pramoth on 9/29/2016 AD.

Diff for: ‎src/test/resources/application.properties

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,15 @@
22
spring.jpa.hibernate.ddl-auto=create-drop
33
spring.jpa.properties.hibernate.cache.use_second_level_cache=false
44
spring.jpa.hibernate.use-new-id-generator-mappings=true
5-
spring.jpa.show-sql=true
65
################## spring.datasource
76
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_ON_EXIT=FALSE
87
# spring.datasource.username=sa
98
# spring.datasource.password=sa
109
spring.datasource.driver-class-name=org.h2.Driver
1110
spring.datasource.platform=h2
1211
spring.datasource.sql-script-encoding=UTF-8
12+
spring.jpa.show-sql=false
1313

1414
spring.datasource.initialize=true
1515

16+
logging.level.org.hibernate.type.descriptor.sql=TRACE

0 commit comments

Comments
 (0)
Please sign in to comment.