Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 9543076

Browse files
mp911dechristophstrobl
authored andcommitted
Add support for DTO projections.
See: #2851 Original Pull Request: #2854
1 parent 3c44521 commit 9543076

File tree

9 files changed

+218
-20
lines changed

9 files changed

+218
-20
lines changed
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
[[cassandra.projections]]
1+
[[redis.projections]]
22
= Projections
33

44
include::{commons}@data-commons::page$repositories/projections.adoc[leveloffset=+1]

‎src/main/java/org/springframework/data/redis/aot/RedisRuntimeHints.java‎

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
import org.springframework.data.redis.core.index.IndexConfiguration;
4747
import org.springframework.data.redis.core.mapping.RedisMappingContext;
4848
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
49+
import org.springframework.data.redis.repository.query.RedisPartTreeQuery;
4950
import org.springframework.data.redis.repository.query.RedisQueryCreator;
5051
import org.springframework.data.redis.repository.support.RedisRepositoryFactoryBean;
5152
import org.springframework.lang.Nullable;
@@ -106,15 +107,15 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader)
106107
TypeReference.of(ReactiveClusterScriptingCommands.class),
107108
TypeReference.of(ReactiveClusterGeoCommands.class),
108109
TypeReference.of(ReactiveClusterHyperLogLogCommands.class), TypeReference.of(ReactiveRedisOperations.class),
109-
TypeReference.of(ReactiveRedisConnectionFactory.class),
110-
TypeReference.of(ReactiveRedisTemplate.class), TypeReference.of(RedisOperations.class),
111-
TypeReference.of(RedisTemplate.class), TypeReference.of(StringRedisTemplate.class),
112-
TypeReference.of(KeyspaceConfiguration.class), TypeReference.of(MappingConfiguration.class),
113-
TypeReference.of(MappingRedisConverter.class), TypeReference.of(RedisConverter.class),
114-
TypeReference.of(RedisCustomConversions.class), TypeReference.of(ReferenceResolver.class),
115-
TypeReference.of(ReferenceResolverImpl.class), TypeReference.of(IndexConfiguration.class),
116-
TypeReference.of(ConfigurableIndexDefinitionProvider.class), TypeReference.of(RedisMappingContext.class),
117-
TypeReference.of(RedisRepositoryFactoryBean.class), TypeReference.of(RedisQueryCreator.class),
110+
TypeReference.of(ReactiveRedisConnectionFactory.class),TypeReference.of(ReactiveRedisTemplate.class),
111+
TypeReference.of(RedisOperations.class), TypeReference.of(RedisTemplate.class),
112+
TypeReference.of(StringRedisTemplate.class), TypeReference.of(KeyspaceConfiguration.class),
113+
TypeReference.of(MappingConfiguration.class), TypeReference.of(MappingRedisConverter.class),
114+
TypeReference.of(RedisConverter.class), TypeReference.of(RedisCustomConversions.class),
115+
TypeReference.of(ReferenceResolver.class), TypeReference.of(ReferenceResolverImpl.class),
116+
TypeReference.of(IndexConfiguration.class), TypeReference.of(ConfigurableIndexDefinitionProvider.class),
117+
TypeReference.of(RedisMappingContext.class), TypeReference.of(RedisRepositoryFactoryBean.class),
118+
TypeReference.of(RedisQueryCreator.class), TypeReference.of(RedisPartTreeQuery.class),
118119
TypeReference.of(MessageListener.class), TypeReference.of(RedisMessageListenerContainer.class),
119120

120121
TypeReference

‎src/main/java/org/springframework/data/redis/core/convert/MappingRedisConverter.java‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,17 @@
1616
package org.springframework.data.redis.core.convert;
1717

1818
import java.lang.reflect.Array;
19-
import java.util.*;
19+
import java.util.ArrayList;
20+
import java.util.Collection;
21+
import java.util.Collections;
22+
import java.util.Comparator;
23+
import java.util.HashMap;
24+
import java.util.Iterator;
25+
import java.util.List;
26+
import java.util.Map;
2027
import java.util.Map.Entry;
28+
import java.util.Optional;
29+
import java.util.Set;
2130
import java.util.regex.Matcher;
2231
import java.util.regex.Pattern;
2332

@@ -1046,6 +1055,11 @@ public IndexResolver getIndexResolver() {
10461055
return this.indexResolver;
10471056
}
10481057

1058+
@Override
1059+
public EntityInstantiators getEntityInstantiators() {
1060+
return entityInstantiators;
1061+
}
1062+
10491063
@Override
10501064
public ConversionService getConversionService() {
10511065
return this.conversionService;

‎src/main/java/org/springframework/data/redis/core/convert/RedisConverter.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
package org.springframework.data.redis.core.convert;
1717

1818
import org.springframework.data.convert.EntityConverter;
19+
import org.springframework.data.mapping.model.EntityInstantiators;
1920
import org.springframework.data.redis.core.mapping.RedisMappingContext;
2021
import org.springframework.data.redis.core.mapping.RedisPersistentEntity;
2122
import org.springframework.data.redis.core.mapping.RedisPersistentProperty;
@@ -40,4 +41,10 @@ public interface RedisConverter
4041
*/
4142
@Nullable
4243
IndexResolver getIndexResolver();
44+
45+
/**
46+
* @return the configured {@link EntityInstantiators}.
47+
* @since 3.2.4
48+
*/
49+
EntityInstantiators getEntityInstantiators();
4350
}

‎src/main/java/org/springframework/data/redis/repository/configuration/EnableRedisRepositories.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import org.springframework.data.redis.core.convert.KeyspaceConfiguration;
3434
import org.springframework.data.redis.core.index.IndexConfiguration;
3535
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
36+
import org.springframework.data.redis.repository.query.RedisPartTreeQuery;
3637
import org.springframework.data.redis.repository.query.RedisQueryCreator;
3738
import org.springframework.data.redis.repository.support.RedisRepositoryFactoryBean;
3839
import org.springframework.data.repository.config.DefaultRepositoryBaseClass;
@@ -52,7 +53,7 @@
5253
@Documented
5354
@Inherited
5455
@Import(RedisRepositoriesRegistrar.class)
55-
@QueryCreatorType(RedisQueryCreator.class)
56+
@QueryCreatorType(value = RedisQueryCreator.class, repositoryQueryType = RedisPartTreeQuery.class)
5657
public @interface EnableRedisRepositories {
5758

5859
/**
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Copyright 2024 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 org.springframework.data.redis.repository.query;
17+
18+
import java.util.ArrayList;
19+
import java.util.Collection;
20+
import java.util.LinkedHashSet;
21+
import java.util.List;
22+
import java.util.Set;
23+
24+
import org.springframework.core.convert.converter.Converter;
25+
import org.springframework.data.convert.DtoInstantiatingConverter;
26+
import org.springframework.data.keyvalue.core.KeyValueOperations;
27+
import org.springframework.data.keyvalue.core.query.KeyValueQuery;
28+
import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery;
29+
import org.springframework.data.mapping.PersistentEntity;
30+
import org.springframework.data.mapping.PersistentProperty;
31+
import org.springframework.data.mapping.context.MappingContext;
32+
import org.springframework.data.mapping.model.EntityInstantiators;
33+
import org.springframework.data.redis.core.RedisKeyValueAdapter;
34+
import org.springframework.data.redis.core.convert.RedisConverter;
35+
import org.springframework.data.repository.query.ParameterAccessor;
36+
import org.springframework.data.repository.query.ParametersParameterAccessor;
37+
import org.springframework.data.repository.query.QueryMethod;
38+
import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider;
39+
import org.springframework.data.repository.query.ResultProcessor;
40+
import org.springframework.data.repository.query.ReturnedType;
41+
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
42+
import org.springframework.data.util.ReflectionUtils;
43+
import org.springframework.data.util.Streamable;
44+
import org.springframework.util.Assert;
45+
import org.springframework.util.ClassUtils;
46+
47+
/**
48+
* Redis-specific implementation of {@link KeyValuePartTreeQuery} supporting projections.
49+
*
50+
* @author Mark Paluch
51+
* @since 3.2.4
52+
*/
53+
public class RedisPartTreeQuery extends KeyValuePartTreeQuery {
54+
55+
private final RedisKeyValueAdapter adapter;
56+
57+
public RedisPartTreeQuery(QueryMethod queryMethod, QueryMethodEvaluationContextProvider evaluationContextProvider,
58+
KeyValueOperations template, Class<? extends AbstractQueryCreator<?, ?>> queryCreator) {
59+
super(queryMethod, evaluationContextProvider, template, queryCreator);
60+
this.adapter = (RedisKeyValueAdapter) template.getKeyValueAdapter();
61+
}
62+
63+
@Override
64+
public Object execute(Object[] parameters) {
65+
66+
ParameterAccessor accessor = new ParametersParameterAccessor(getQueryMethod().getParameters(), parameters);
67+
KeyValueQuery<?> query = prepareQuery(parameters);
68+
ResultProcessor processor = getQueryMethod().getResultProcessor().withDynamicProjection(accessor);
69+
70+
RedisConverter converter = adapter.getConverter();
71+
Converter<Object, Object> resultPostProcessor = new ResultProcessingConverter(processor,
72+
converter.getMappingContext(), converter.getEntityInstantiators());
73+
74+
Object source = doExecute(parameters, query);
75+
return source != null ? processor.processResult(resultPostProcessor.convert(source)) : null;
76+
}
77+
78+
/**
79+
* A {@link Converter} to post-process all source objects using the given {@link ResultProcessor}.
80+
*
81+
* @author Mark Paluch
82+
*/
83+
static final class ResultProcessingConverter implements Converter<Object, Object> {
84+
85+
private final ResultProcessor processor;
86+
private final MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context;
87+
private final EntityInstantiators instantiators;
88+
89+
public ResultProcessingConverter(ResultProcessor processor,
90+
MappingContext<? extends PersistentEntity<?, ?>, ? extends PersistentProperty<?>> context,
91+
EntityInstantiators instantiators) {
92+
93+
Assert.notNull(processor, "Processor must not be null!");
94+
Assert.notNull(context, "MappingContext must not be null!");
95+
Assert.notNull(instantiators, "Instantiators must not be null!");
96+
97+
this.processor = processor;
98+
this.context = context;
99+
this.instantiators = instantiators;
100+
}
101+
102+
/*
103+
* (non-Javadoc)
104+
* @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
105+
*/
106+
@Override
107+
public Object convert(Object source) {
108+
109+
if (source instanceof Set<?> s) {
110+
111+
Set<Object> target = new LinkedHashSet<>(s.size());
112+
113+
for (Object o : s) {
114+
target.add(convert(o));
115+
}
116+
117+
return target;
118+
}
119+
120+
if (source instanceof Collection<?> c) {
121+
122+
List<Object> target = new ArrayList<>(c.size());
123+
124+
for (Object o : c) {
125+
target.add(convert(o));
126+
}
127+
128+
return target;
129+
}
130+
131+
if (source instanceof Streamable<?> s) {
132+
return s.map(this::convert);
133+
}
134+
135+
ReturnedType returnedType = processor.getReturnedType();
136+
137+
if (ReflectionUtils.isVoid(returnedType.getReturnedType())) {
138+
return null;
139+
}
140+
141+
if (ClassUtils.isPrimitiveOrWrapper(returnedType.getReturnedType())) {
142+
return source;
143+
}
144+
145+
Converter<Object, Object> converter = new DtoInstantiatingConverter(returnedType.getReturnedType(), context,
146+
instantiators);
147+
148+
return processor.processResult(source, converter);
149+
}
150+
}
151+
}

‎src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactory.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,11 @@
1616
package org.springframework.data.redis.repository.support;
1717

1818
import org.springframework.data.keyvalue.core.KeyValueOperations;
19-
import org.springframework.data.keyvalue.repository.query.KeyValuePartTreeQuery;
2019
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactory;
2120
import org.springframework.data.redis.core.mapping.RedisMappingContext;
2221
import org.springframework.data.redis.core.mapping.RedisPersistentEntity;
2322
import org.springframework.data.redis.repository.core.MappingRedisEntityInformation;
23+
import org.springframework.data.redis.repository.query.RedisPartTreeQuery;
2424
import org.springframework.data.redis.repository.query.RedisQueryCreator;
2525
import org.springframework.data.repository.core.EntityInformation;
2626
import org.springframework.data.repository.core.RepositoryMetadata;
@@ -59,7 +59,7 @@ public RedisRepositoryFactory(KeyValueOperations keyValueOperations) {
5959
*/
6060
public RedisRepositoryFactory(KeyValueOperations keyValueOperations,
6161
Class<? extends AbstractQueryCreator<?, ?>> queryCreator) {
62-
this(keyValueOperations, queryCreator, KeyValuePartTreeQuery.class);
62+
this(keyValueOperations, queryCreator, RedisPartTreeQuery.class);
6363
}
6464

6565
/**

‎src/main/java/org/springframework/data/redis/repository/support/RedisRepositoryFactoryBean.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.springframework.beans.factory.FactoryBean;
1919
import org.springframework.data.keyvalue.core.KeyValueOperations;
2020
import org.springframework.data.keyvalue.repository.support.KeyValueRepositoryFactoryBean;
21+
import org.springframework.data.redis.repository.query.RedisPartTreeQuery;
2122
import org.springframework.data.repository.Repository;
2223
import org.springframework.data.repository.query.RepositoryQuery;
2324
import org.springframework.data.repository.query.parser.AbstractQueryCreator;
@@ -44,6 +45,7 @@ public class RedisRepositoryFactoryBean<T extends Repository<S, ID>, S, ID>
4445
*/
4546
public RedisRepositoryFactoryBean(Class<? extends T> repositoryInterface) {
4647
super(repositoryInterface);
48+
setQueryType(RedisPartTreeQuery.class);
4749
}
4850

4951
@Override

‎src/test/java/org/springframework/data/redis/repository/RedisRepositoryIntegrationTestBase.java‎

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
import org.springframework.data.repository.CrudRepository;
5252
import org.springframework.data.repository.PagingAndSortingRepository;
5353
import org.springframework.data.repository.query.QueryByExampleExecutor;
54+
import org.springframework.data.util.Streamable;
5455
import org.springframework.lang.Nullable;
5556

5657
/**
@@ -130,9 +131,13 @@ void shouldProjectSingleResult() {
130131

131132
repo.saveAll(Arrays.asList(rand, egwene));
132133

133-
PersonProjection projectionById = repo.findProjectionById(rand.getId());
134-
assertThat(projectionById).isNotNull();
135-
assertThat(projectionById.getFirstname()).isEqualTo(rand.firstname);
134+
PersonProjection projection = repo.findProjectionById(rand.getId(), PersonProjection.class);
135+
assertThat(projection).isNotNull();
136+
assertThat(projection.getFirstname()).isEqualTo(rand.firstname);
137+
138+
PersonDto dto = repo.findProjectionById(rand.getId(), PersonDto.class);
139+
assertThat(dto).isNotNull();
140+
assertThat(dto.firstname()).isEqualTo(rand.firstname);
136141
}
137142

138143
@Test // GH-2851
@@ -147,10 +152,20 @@ void shouldProjectCollection() {
147152

148153
repo.saveAll(Arrays.asList(rand, egwene));
149154

150-
List<PersonProjection> projectionById = repo.findProjectionBy();
151-
assertThat(projectionById).hasSize(2) //
155+
List<PersonProjection> projection = repo.findProjectionBy();
156+
assertThat(projection).hasSize(2) //
152157
.extracting(PersonProjection::getFirstname) //
153158
.contains(rand.getFirstname(), egwene.getFirstname());
159+
160+
projection = repo.findProjectionStreamBy().toList();
161+
assertThat(projection).hasSize(2) //
162+
.extracting(PersonProjection::getFirstname) //
163+
.contains(rand.getFirstname(), egwene.getFirstname());
164+
165+
List<PersonDto> dtos = repo.findProjectionDtoBy();
166+
assertThat(dtos).hasSize(2) //
167+
.extracting(PersonDto::firstname) //
168+
.contains(rand.getFirstname(), egwene.getFirstname());
154169
}
155170

156171
@Test // DATAREDIS-425
@@ -624,10 +639,14 @@ public interface PersonRepository extends PagingAndSortingRepository<Person, Str
624639

625640
Person findEntityById(String id);
626641

627-
PersonProjection findProjectionById(String id);
642+
<T> T findProjectionById(String id, Class<T> projection);
643+
644+
Streamable<PersonProjection> findProjectionStreamBy();
628645

629646
List<PersonProjection> findProjectionBy();
630647

648+
List<PersonDto> findProjectionDtoBy();
649+
631650
@Override
632651
<S extends Person> List<S> findAll(Example<S> example);
633652
}
@@ -636,6 +655,9 @@ public interface PersonProjection {
636655
String getFirstname();
637656
}
638657

658+
record PersonDto(String firstname) {
659+
}
660+
639661
public interface CityRepository extends CrudRepository<City, String> {
640662

641663
List<City> findByLocationNear(Point point, Distance distance);

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /